mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 11:16:47 +00:00
refactor(templates): the host pages' scripts load from static files
The host pages lose their last inline scripts: the overview's charts (host_graphs.js), the New pool, New network and New interface dialogs (pool_dialog.js, network_dialog.js, interface_dialog.js), a network's and a network filter's XML editors and dialogs (network_page.js, nwfilter_page.js, nwfilters_page.js), the secrets page's labels (secrets_page.js, its texts from the view as usage_texts) and a pool's page with the ISO upload (storage_page.js). Their URLs and messages are data attributes of the elements they belong to; the upload reads its token and address from its form. No template has an inline script or an inline event handler any more.
This commit is contained in:
parent
946629b035
commit
50dcaba699
23 changed files with 431 additions and 407 deletions
|
|
@ -108,7 +108,7 @@
|
|||
</div>
|
||||
<div class="small text-body-secondary mt-2 mb-4">
|
||||
{% trans "The last 10 readings, one every 5 seconds. Memory in use leaves out the host's buffers and cache." %}
|
||||
<span id="graph_state" data-updated="{% trans "Updated at %(time)s." %}"
|
||||
<span id="graph_state" data-url="{% url 'compute_graph' compute.id %}" data-average-over="{% trans "average over" %}" data-updated="{% trans "Updated at %(time)s." %}"
|
||||
data-stale="{% trans "Not updated since %(time)s: the host did not answer." %}">{% trans "Waiting for the first reading." %}</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -194,61 +194,6 @@
|
|||
<script src="{% static "js/chart.umd.min.js" %}"></script>
|
||||
<script src="{% static "js/charts.js" %}"></script>
|
||||
<script src="{% static "js/poll.js" %}"></script>
|
||||
<script>
|
||||
{% trans "average over" as average_over %}
|
||||
// the seconds each CPU point averages
|
||||
var cpuWindows = [];
|
||||
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
|
||||
max: 100,
|
||||
tooltip: function (context, text) {
|
||||
var span = cpuWindows[context.dataIndex];
|
||||
return span ? text + ' ({{ average_over|escapejs }} ' + span + ' s)' : text;
|
||||
},
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "GB", ["Usage"]);
|
||||
var cpuNow = document.getElementById("cpu_now");
|
||||
var memNow = document.getElementById("mem_now");
|
||||
var state = document.getElementById("graph_state");
|
||||
var lastUpdate = "–";
|
||||
|
||||
function gb(bytes) {
|
||||
return Math.round(bytes / 107374182.4) / 10;
|
||||
}
|
||||
|
||||
// a failed reading leaves the charts as they were: say since when
|
||||
function stale() {
|
||||
state.textContent = state.dataset.stale.replace("%(time)s", lastUpdate);
|
||||
state.classList.add("text-danger");
|
||||
}
|
||||
|
||||
// the CPU sample of the previous answer: usage is taken since then
|
||||
var cpuSample = null;
|
||||
pollWhileVisible(5000, function () {
|
||||
return $.getJSON('{% url 'compute_graph' compute.id %}', cpuSample ? {cpu_sample: cpuSample} : {}).fail(stale);
|
||||
}, function (data) {
|
||||
cpuSample = data.cpu_sample || null;
|
||||
cpuWindows.push(data.window);
|
||||
if (cpuWindows.length > CHART_POINTS) {
|
||||
cpuWindows.shift();
|
||||
}
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
// a host that could not be read gives no values: a gap, same axis
|
||||
if (data.memdata.total) {
|
||||
memChart.options.scales.y.max = gb(data.memdata.total);
|
||||
addPoint(memChart, data.timeline, [gb(data.memdata.usage)]);
|
||||
memNow.textContent = gb(data.memdata.usage) + " / " + gb(data.memdata.total) + " GB";
|
||||
} else {
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
}
|
||||
if (data.cpudata == null) {
|
||||
stale();
|
||||
return;
|
||||
}
|
||||
cpuNow.textContent = data.cpudata + " %";
|
||||
lastUpdate = data.timeline;
|
||||
state.textContent = state.dataset.updated.replace("%(time)s", data.timeline);
|
||||
state.classList.remove("text-danger");
|
||||
}, 0);
|
||||
</script>
|
||||
<script src="{% static 'js/host_graphs.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -143,12 +143,17 @@ class GraphScriptTestCase(TestCase):
|
|||
details.return_value.get_node_info.return_value = ("h", "x86_64", 1024, 2, "cpu", "qemu+tcp://h/system")
|
||||
details.return_value.get_cpu_topology.return_value = TOPOLOGY
|
||||
page = self.client.get(reverse("overview", args=[compute.id])).content.decode()
|
||||
script = next(s for s in html.fromstring(page).xpath("//script[not(@src)]/text()") if "pollWhileVisible(" in s)
|
||||
doc = html.fromstring(page)
|
||||
self.assertIn("/static/js/host_graphs.js", doc.xpath("//script/@src"))
|
||||
(state,) = doc.xpath("//span[@id='graph_state']")
|
||||
dataset = {"url": state.get("data-url"), "averageOver": state.get("data-average-over"),
|
||||
"updated": "Updated at %(time)s.", "stale": "Not updated since %(time)s."}
|
||||
script = (settings.BASE_DIR / "static/js/host_graphs.js").read_text()
|
||||
js = """
|
||||
const vm = require("vm");
|
||||
const charts = [], requests = [];
|
||||
const element = (dataset) => ({dataset, textContent: "", classList: {add: () => {}, remove: () => {}}});
|
||||
const elements = {graph_state: element({updated: "Updated at %(time)s.", stale: "Not updated since %(time)s."}),
|
||||
const elements = {graph_state: element(DATASET),
|
||||
cpu_now: element({}), mem_now: element({})};
|
||||
let poll;
|
||||
const page = {
|
||||
|
|
@ -180,7 +185,8 @@ class GraphScriptTestCase(TestCase):
|
|||
console.log(JSON.stringify({requests, label, shown, state: elements.graph_state.textContent,
|
||||
cpu: charts[0].data.datasets[0].data,
|
||||
mem: charts[1].data.datasets[0].data, memMax: charts[1].options.scales.y.max}));
|
||||
""".replace("SCRIPT", json.dumps(script)).replace("CHARTS", json.dumps(CHARTS_JS.read_text()))
|
||||
""".replace("SCRIPT", json.dumps(script)).replace("CHARTS", json.dumps(CHARTS_JS.read_text())).replace(
|
||||
"DATASET", json.dumps(dataset))
|
||||
out = subprocess.run(["node", "-e", js], capture_output=True, text=True, timeout=30)
|
||||
self.assertEqual(out.returncode, 0, out.stderr)
|
||||
result = json.loads(out.stdout)
|
||||
|
|
|
|||
|
|
@ -151,18 +151,5 @@
|
|||
</div>
|
||||
</div>
|
||||
<script src="{% static 'js/choice_dialog.js' %}"></script>
|
||||
<script>
|
||||
// a static address's fields show with its choice (after the page's
|
||||
// scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('interface-form');
|
||||
form.querySelectorAll('select[data-static]').forEach(function (method) {
|
||||
var fields = form.querySelector(method.dataset.static);
|
||||
function show() { fields.hidden = fields.disabled = method.value !== 'static'; }
|
||||
method.addEventListener('change', show);
|
||||
show();
|
||||
});
|
||||
choiceDialog(document.getElementById('AddInterface'), 'itype', function () {});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/interface_dialog.js' %}"></script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -130,22 +130,5 @@
|
|||
</div>
|
||||
</div>
|
||||
<script src="{% static 'js/choice_dialog.js' %}"></script>
|
||||
<script>
|
||||
// the IPv6 fields show with their box; a NAT or routed network needs a
|
||||
// subnet (after the page's scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var ipv6 = document.getElementById('net_ipv6');
|
||||
function showIpv6() {
|
||||
var fields = document.getElementById('net_ipv6_fields');
|
||||
fields.hidden = !ipv6.checked;
|
||||
fields.querySelectorAll('input').forEach(function (input) { input.disabled = !ipv6.checked; });
|
||||
document.getElementById('net_subnet6').required = ipv6.checked;
|
||||
}
|
||||
ipv6.addEventListener('change', showIpv6);
|
||||
choiceDialog(document.getElementById('AddNetPool'), 'forward', function (mode) {
|
||||
document.getElementById('net_subnet').required = mode === 'nat' || mode === 'route';
|
||||
showIpv6();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/network_dialog.js' %}"></script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -449,47 +449,5 @@
|
|||
{% block script %}
|
||||
{% if fixed_addresses %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
|
||||
<script src="{% static "js/ace/ace.js" %}"></script>
|
||||
<script>
|
||||
var editor = ace.edit("edit_editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
var editInput = $('input[name="edit_xml"]');
|
||||
editor.getSession().on("change", function () {
|
||||
editInput.val(editor.getSession().getValue());
|
||||
});
|
||||
// the editor measures itself when its section is shown
|
||||
document.getElementById("network-xml").addEventListener("shown.bs.collapse", () => editor.resize());
|
||||
|
||||
// a dialog opened by a row's button gets that row's values, one opened by an Add button starts empty
|
||||
function fillFrom(dialogId, fill) {
|
||||
var dialog = document.getElementById(dialogId);
|
||||
if (dialog) dialog.addEventListener("show.bs.modal", function (event) {
|
||||
fill(dialog.querySelector("form"), event.relatedTarget.dataset);
|
||||
});
|
||||
}
|
||||
fillFrom("FixedAddress", function (form, data) {
|
||||
form.elements.original.value = data.original || "";
|
||||
form.elements.address.value = data.address || "";
|
||||
form.elements.mac.value = data.mac || "";
|
||||
form.elements.name.value = data.name || "";
|
||||
// Add starts from the first subnet, not the one a row chose before
|
||||
form.elements.ip_index.value = data.ipIndex || form.elements.ip_index.options[0].value;
|
||||
document.getElementById("fixed_subnet_row").hidden = Boolean(data.original);
|
||||
});
|
||||
fillFrom("DhcpRange", function (form, data) {
|
||||
form.elements.ip_index.value = data.ipIndex;
|
||||
form.elements.range_start.value = data.start;
|
||||
form.elements.range_end.value = data.end;
|
||||
document.getElementById("dhcp_subnet").textContent = data.subnet;
|
||||
});
|
||||
fillFrom("QosLimit", function (form, data) {
|
||||
var direction = form.elements.qos_direction;
|
||||
direction.querySelectorAll("option").forEach(function (option) {
|
||||
option.disabled = data.direction ? option.value !== data.direction : option.hasAttribute("data-taken");
|
||||
});
|
||||
direction.value = data.direction || direction.querySelector("option:not(:disabled)").value;
|
||||
form.elements.qos_average.value = data.average || "";
|
||||
form.elements.qos_peak.value = data.peak || "";
|
||||
form.elements.qos_burst.value = data.burst || "";
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/network_page.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -187,24 +187,6 @@
|
|||
{% block script %}
|
||||
{% if uuid %}
|
||||
<script src="{% static 'js/ace/ace.js' %}"></script>
|
||||
<script>
|
||||
// each editor's text goes in its form's hidden field, the starter too if sent as it is,
|
||||
// and it measures itself when its section or dialog is shown
|
||||
function xmlEditor(textareaId, fieldName, shownBy, shownEvent) {
|
||||
var textarea = document.getElementById(textareaId);
|
||||
var form = textarea.closest("form");
|
||||
var editor = ace.edit(textareaId);
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
editor.setOption("textInputAriaLabel", textarea.getAttribute("aria-label"));
|
||||
var field = form.elements[fieldName];
|
||||
field.value = editor.getSession().getValue();
|
||||
editor.getSession().on("change", function () {
|
||||
field.value = editor.getSession().getValue();
|
||||
});
|
||||
document.getElementById(shownBy).addEventListener(shownEvent, () => editor.resize());
|
||||
}
|
||||
xmlEditor("edit_editor", "edit_xml", "nwfilter-xml", "shown.bs.collapse");
|
||||
xmlEditor("rule_editor", "nwfilterrule_xml", "AddNWFilterRule", "shown.bs.modal");
|
||||
</script>
|
||||
<script src="{% static 'js/nwfilter_page.js' %}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -100,28 +100,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% if nwfilters %}
|
||||
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||
<script>
|
||||
document.getElementById("CloneNWFilter").addEventListener("show.bs.modal", function (event) {
|
||||
var name = event.relatedTarget.dataset.name;
|
||||
this.querySelector("form").elements.nwfiltername.value = name;
|
||||
document.getElementById("clone_source").textContent = name;
|
||||
document.getElementById("clone_name").value = name + "-copy";
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
{% if nwfilters %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
|
||||
<script src="{% static 'js/ace/ace.js' %}"></script>
|
||||
<script>
|
||||
var editor = ace.edit("create_editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
editor.setOption("textInputAriaLabel", "{{ _('Filter XML')|escapejs }}");
|
||||
var input = $('input[name="nwfilter_xml"]');
|
||||
input.val(editor.getSession().getValue()); // the starter too, if sent as it is
|
||||
editor.getSession().on("change", function () {
|
||||
input.val(editor.getSession().getValue());
|
||||
});
|
||||
// the editor measures itself when its dialog is shown
|
||||
document.getElementById("AddNWFilter").addEventListener("shown.bs.modal", () => editor.resize());
|
||||
</script>
|
||||
<script src="{% static 'js/nwfilters_page.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
54
static/js/host_graphs.js
Normal file
54
static/js/host_graphs.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// The host's CPU and memory charts (overview.html), polled every 5 s.
|
||||
var state = document.getElementById("graph_state");
|
||||
// the seconds each CPU point averages
|
||||
var cpuWindows = [];
|
||||
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
|
||||
max: 100,
|
||||
tooltip: function (context, text) {
|
||||
var span = cpuWindows[context.dataIndex];
|
||||
return span ? text + ' (' + state.dataset.averageOver + ' ' + span + ' s)' : text;
|
||||
},
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "GB", ["Usage"]);
|
||||
var cpuNow = document.getElementById("cpu_now");
|
||||
var memNow = document.getElementById("mem_now");
|
||||
var lastUpdate = "–";
|
||||
|
||||
function gb(bytes) {
|
||||
return Math.round(bytes / 107374182.4) / 10;
|
||||
}
|
||||
|
||||
// a failed reading leaves the charts as they were: say since when
|
||||
function stale() {
|
||||
state.textContent = state.dataset.stale.replace("%(time)s", lastUpdate);
|
||||
state.classList.add("text-danger");
|
||||
}
|
||||
|
||||
// the CPU sample of the previous answer: usage is taken since then
|
||||
var cpuSample = null;
|
||||
pollWhileVisible(5000, function () {
|
||||
return $.getJSON(state.dataset.url, cpuSample ? {cpu_sample: cpuSample} : {}).fail(stale);
|
||||
}, function (data) {
|
||||
cpuSample = data.cpu_sample || null;
|
||||
cpuWindows.push(data.window);
|
||||
if (cpuWindows.length > CHART_POINTS) {
|
||||
cpuWindows.shift();
|
||||
}
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
// a host that could not be read gives no values: a gap, same axis
|
||||
if (data.memdata.total) {
|
||||
memChart.options.scales.y.max = gb(data.memdata.total);
|
||||
addPoint(memChart, data.timeline, [gb(data.memdata.usage)]);
|
||||
memNow.textContent = gb(data.memdata.usage) + " / " + gb(data.memdata.total) + " GB";
|
||||
} else {
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
}
|
||||
if (data.cpudata == null) {
|
||||
stale();
|
||||
return;
|
||||
}
|
||||
cpuNow.textContent = data.cpudata + " %";
|
||||
lastUpdate = data.timeline;
|
||||
state.textContent = state.dataset.updated.replace("%(time)s", data.timeline);
|
||||
state.classList.remove("text-danger");
|
||||
}, 0);
|
||||
13
static/js/interface_dialog.js
Normal file
13
static/js/interface_dialog.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// The New interface dialog (create_iface_block.html).
|
||||
// a static address's fields show with its choice (after the page's
|
||||
// scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('interface-form');
|
||||
form.querySelectorAll('select[data-static]').forEach(function (method) {
|
||||
var fields = form.querySelector(method.dataset.static);
|
||||
function show() { fields.hidden = fields.disabled = method.value !== 'static'; }
|
||||
method.addEventListener('change', show);
|
||||
show();
|
||||
});
|
||||
choiceDialog(document.getElementById('AddInterface'), 'itype', function () {});
|
||||
});
|
||||
17
static/js/network_dialog.js
Normal file
17
static/js/network_dialog.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// The New network dialog (create_net_block.html).
|
||||
// the IPv6 fields show with their box; a NAT or routed network needs a
|
||||
// subnet (after the page's scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var ipv6 = document.getElementById('net_ipv6');
|
||||
function showIpv6() {
|
||||
var fields = document.getElementById('net_ipv6_fields');
|
||||
fields.hidden = !ipv6.checked;
|
||||
fields.querySelectorAll('input').forEach(function (input) { input.disabled = !ipv6.checked; });
|
||||
document.getElementById('net_subnet6').required = ipv6.checked;
|
||||
}
|
||||
ipv6.addEventListener('change', showIpv6);
|
||||
choiceDialog(document.getElementById('AddNetPool'), 'forward', function (mode) {
|
||||
document.getElementById('net_subnet').required = mode === 'nat' || mode === 'route';
|
||||
showIpv6();
|
||||
});
|
||||
});
|
||||
42
static/js/network_page.js
Normal file
42
static/js/network_page.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// A network's page (network.html): its XML editor and the dialogs its rows open.
|
||||
var editor = ace.edit("edit_editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
var editInput = $('input[name="edit_xml"]');
|
||||
editor.getSession().on("change", function () {
|
||||
editInput.val(editor.getSession().getValue());
|
||||
});
|
||||
// the editor measures itself when its section is shown
|
||||
document.getElementById("network-xml").addEventListener("shown.bs.collapse", () => editor.resize());
|
||||
|
||||
// a dialog opened by a row's button gets that row's values, one opened by an Add button starts empty
|
||||
function fillFrom(dialogId, fill) {
|
||||
var dialog = document.getElementById(dialogId);
|
||||
if (dialog) dialog.addEventListener("show.bs.modal", function (event) {
|
||||
fill(dialog.querySelector("form"), event.relatedTarget.dataset);
|
||||
});
|
||||
}
|
||||
fillFrom("FixedAddress", function (form, data) {
|
||||
form.elements.original.value = data.original || "";
|
||||
form.elements.address.value = data.address || "";
|
||||
form.elements.mac.value = data.mac || "";
|
||||
form.elements.name.value = data.name || "";
|
||||
// Add starts from the first subnet, not the one a row chose before
|
||||
form.elements.ip_index.value = data.ipIndex || form.elements.ip_index.options[0].value;
|
||||
document.getElementById("fixed_subnet_row").hidden = Boolean(data.original);
|
||||
});
|
||||
fillFrom("DhcpRange", function (form, data) {
|
||||
form.elements.ip_index.value = data.ipIndex;
|
||||
form.elements.range_start.value = data.start;
|
||||
form.elements.range_end.value = data.end;
|
||||
document.getElementById("dhcp_subnet").textContent = data.subnet;
|
||||
});
|
||||
fillFrom("QosLimit", function (form, data) {
|
||||
var direction = form.elements.qos_direction;
|
||||
direction.querySelectorAll("option").forEach(function (option) {
|
||||
option.disabled = data.direction ? option.value !== data.direction : option.hasAttribute("data-taken");
|
||||
});
|
||||
direction.value = data.direction || direction.querySelector("option:not(:disabled)").value;
|
||||
form.elements.qos_average.value = data.average || "";
|
||||
form.elements.qos_peak.value = data.peak || "";
|
||||
form.elements.qos_burst.value = data.burst || "";
|
||||
});
|
||||
18
static/js/nwfilter_page.js
Normal file
18
static/js/nwfilter_page.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// A network filter's page (nwfilter.html).
|
||||
// each editor's text goes in its form's hidden field, the starter too if sent as it is,
|
||||
// and it measures itself when its section or dialog is shown
|
||||
function xmlEditor(textareaId, fieldName, shownBy, shownEvent) {
|
||||
var textarea = document.getElementById(textareaId);
|
||||
var form = textarea.closest("form");
|
||||
var editor = ace.edit(textareaId);
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
editor.setOption("textInputAriaLabel", textarea.getAttribute("aria-label"));
|
||||
var field = form.elements[fieldName];
|
||||
field.value = editor.getSession().getValue();
|
||||
editor.getSession().on("change", function () {
|
||||
field.value = editor.getSession().getValue();
|
||||
});
|
||||
document.getElementById(shownBy).addEventListener(shownEvent, () => editor.resize());
|
||||
}
|
||||
xmlEditor("edit_editor", "edit_xml", "nwfilter-xml", "shown.bs.collapse");
|
||||
xmlEditor("rule_editor", "nwfilterrule_xml", "AddNWFilterRule", "shown.bs.modal");
|
||||
23
static/js/nwfilters_page.js
Normal file
23
static/js/nwfilters_page.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// The network filters page (nwfilters.html): the Clone dialog takes the
|
||||
// filter of its row, and the New filter dialog's XML editor fills its form.
|
||||
var cloneDialog = document.getElementById("CloneNWFilter");
|
||||
if (cloneDialog) {
|
||||
cloneDialog.addEventListener("show.bs.modal", function (event) {
|
||||
var name = event.relatedTarget.dataset.name;
|
||||
this.querySelector("form").elements.nwfiltername.value = name;
|
||||
document.getElementById("clone_source").textContent = name;
|
||||
document.getElementById("clone_name").value = name + "-copy";
|
||||
});
|
||||
}
|
||||
|
||||
var textarea = document.getElementById("create_editor");
|
||||
var editor = ace.edit("create_editor");
|
||||
editor.getSession().setMode("ace/mode/xml");
|
||||
editor.setOption("textInputAriaLabel", textarea.getAttribute("aria-label"));
|
||||
var input = $('input[name="nwfilter_xml"]');
|
||||
input.val(editor.getSession().getValue()); // the starter too, if sent as it is
|
||||
editor.getSession().on("change", function () {
|
||||
input.val(editor.getSession().getValue());
|
||||
});
|
||||
// the editor measures itself when its dialog is shown
|
||||
document.getElementById("AddNWFilter").addEventListener("shown.bs.modal", () => editor.resize());
|
||||
31
static/js/pool_dialog.js
Normal file
31
static/js/pool_dialog.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// The New pool dialog (create_stg_block.html).
|
||||
// the ISO pool is a directory named iso; a path follows the pool's name
|
||||
// until it is edited (after the page's scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('pool-form');
|
||||
var name = form.elements.namedItem('name');
|
||||
var otherName = name.value;
|
||||
var hasCephSecrets = "cephSecrets" in form.dataset;
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
|
||||
});
|
||||
name.addEventListener('input', function () {
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
var derived = name.value ? path.dataset.followName + name.value : '';
|
||||
if (!path.value || path.value === path.dataset.derived) path.value = derived;
|
||||
path.dataset.derived = derived;
|
||||
});
|
||||
});
|
||||
// a new device or name: the erasing is agreed to again
|
||||
form.querySelectorAll('#pool_lvm_source, #pool_name').forEach(function (field) {
|
||||
field.addEventListener('input', function () { document.getElementById('pool_lvm_erase').checked = false; });
|
||||
});
|
||||
choiceDialog(document.getElementById('AddStgPool'), 'pool_kind', function (kind) {
|
||||
var iso = kind === 'iso';
|
||||
if (!name.readOnly) otherName = name.value;
|
||||
name.value = iso ? 'iso' : otherName;
|
||||
name.readOnly = iso;
|
||||
form.elements.namedItem('stg_type').value = iso ? 'dir' : kind;
|
||||
form.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
|
||||
});
|
||||
});
|
||||
34
static/js/secrets_page.js
Normal file
34
static/js/secrets_page.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// The secrets page (secrets.html): the labels of a secret's usage and
|
||||
// value follow its usage type (usage_texts from the view).
|
||||
var usageTexts = JSON.parse(document.getElementById("usage-texts").textContent);
|
||||
|
||||
var usageType = document.getElementById("secret_usage_type");
|
||||
if (usageType) {
|
||||
var showUsage = function () {
|
||||
var texts = usageTexts[usageType.value];
|
||||
document.getElementById("secret_usage_label").textContent = texts.usage;
|
||||
document.getElementById("secret_usage_help").textContent = texts.usageHelp;
|
||||
document.getElementById("secret_value_label").textContent = texts.value;
|
||||
document.getElementById("secret_value_help").textContent = texts.valueHelp + " " + usageTexts.later;
|
||||
};
|
||||
usageType.addEventListener("change", showUsage);
|
||||
showUsage();
|
||||
}
|
||||
|
||||
var setValue = document.getElementById("SetSecretValue");
|
||||
if (setValue) {
|
||||
setValue.addEventListener("show.bs.modal", function (event) {
|
||||
var row = event.relatedTarget.dataset;
|
||||
var texts = usageTexts[row.type] || usageTexts.none;
|
||||
var form = this.querySelector("form");
|
||||
form.reset(); // no value of another secret stays in the field
|
||||
form.elements.uuid.value = row.uuid;
|
||||
document.getElementById("value_usage").textContent = row.usage;
|
||||
document.getElementById("set_value_label").textContent = texts.value;
|
||||
document.getElementById("set_value_help").textContent = texts.valueHelp;
|
||||
// a passphrase's data stays encrypted with the old one
|
||||
var passphrase = ["volume", "tls", "vtpm"].includes(row.type);
|
||||
document.getElementById("set_value_note").hidden = !passphrase;
|
||||
form.elements.value.setAttribute("aria-describedby", passphrase ? "set_value_help set_value_note" : "set_value_help");
|
||||
});
|
||||
}
|
||||
151
static/js/storage_page.js
Normal file
151
static/js/storage_page.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// A storage pool's page (storage.html): the volume Clone dialog, a refused
|
||||
// new volume's dialog, and the ISO upload in chunks.
|
||||
|
||||
// the clone dialog: filled for the volume whose button opened it, its
|
||||
// choices reset; the format rows show while converting
|
||||
function showCloneRows() {
|
||||
var convert = $('#clone-convert').prop('checked');
|
||||
$('#clone-format-row').toggleClass('d-none', !convert);
|
||||
$('#clone-prealloc-row').toggleClass('d-none', !(convert && $('#clone-format').val() == 'qcow2'));
|
||||
}
|
||||
$('#CloneVolume').on('show.bs.modal', function (event) {
|
||||
var volume = event.relatedTarget.dataset.volume;
|
||||
$('#clone-image').val(volume);
|
||||
$('#clone-image-name').text(volume);
|
||||
$('#clone-name').val('');
|
||||
$('#clone-convert, #clone-prealloc').prop('checked', false);
|
||||
$('#clone-format').val('raw');
|
||||
showCloneRows();
|
||||
});
|
||||
$('#clone-convert, #clone-format').on('change', showCloneRows);
|
||||
|
||||
// a refused new volume: its dialog opens again on what was entered, at the refused field
|
||||
var addImage = document.getElementById('AddImage');
|
||||
if (addImage && addImage.hasAttribute('data-open')) {
|
||||
addImage.addEventListener('shown.bs.modal', focusFirstError, {once: true});
|
||||
bootstrap.Modal.getOrCreateInstance(addImage).show();
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var pendingUpload = null;
|
||||
var uploadInProgress = false;
|
||||
// the browser sends the file: leaving the page stops the upload
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (uploadInProgress) { e.preventDefault(); e.returnValue = ''; }
|
||||
});
|
||||
$('#isoUploadForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
// the form's messages and token; it is sent to this page
|
||||
var form = this;
|
||||
var texts = form.dataset;
|
||||
if (uploadInProgress) return;
|
||||
|
||||
var fileInput = $('#id_file')[0];
|
||||
var errorMessage = $('#upload-error-message');
|
||||
if (fileInput.files.length === 0) {
|
||||
errorMessage.text(texts.noFile).removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
var file = fileInput.files[0];
|
||||
var fileName = file.name;
|
||||
if (!/\.iso$/i.test(fileName) || fileName.length <= 4) {
|
||||
errorMessage.text(texts.notIso).removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
if (file.size === 0) {
|
||||
errorMessage.text(texts.empty).removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
var chunkSize = 32 * 1024 * 1024; // Fewer SSH connections per upload
|
||||
var totalChunks = Math.ceil(file.size / chunkSize);
|
||||
var fileSignature = [fileName, file.size, file.lastModified].join(':');
|
||||
if (!pendingUpload || pendingUpload.signature !== fileSignature) {
|
||||
pendingUpload = {
|
||||
signature: fileSignature,
|
||||
chunkIndex: 0,
|
||||
uploadId: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : (Date.now() + '-' + Math.random().toString(36).substring(2, 10))
|
||||
};
|
||||
}
|
||||
var chunkIndex = pendingUpload.chunkIndex;
|
||||
var uploadId = pendingUpload.uploadId;
|
||||
var retryCount = 0;
|
||||
uploadInProgress = true;
|
||||
|
||||
var uploadDialog = bootstrap.Modal.getOrCreateInstance(document.getElementById('IsoUpload'));
|
||||
var uploadStatus = $('#upload-status');
|
||||
var uploadStatusText = $('#upload-status-text');
|
||||
var uploadButton = $('#isoUploadForm button[type="submit"]');
|
||||
|
||||
errorMessage.addClass("d-none");
|
||||
uploadStatusText.text(texts.starting);
|
||||
uploadStatus.removeClass('d-none').addClass('d-flex');
|
||||
uploadButton.prop('disabled', true);
|
||||
|
||||
function uploadChunk() {
|
||||
if (chunkIndex >= totalChunks) {
|
||||
return;
|
||||
}
|
||||
|
||||
var start = chunkIndex * chunkSize;
|
||||
var end = Math.min(start + chunkSize, file.size);
|
||||
var chunk = file.slice(start, end);
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('file', chunk, fileName);
|
||||
formData.append('file_name', fileName);
|
||||
formData.append('chunk_index', chunkIndex);
|
||||
formData.append('total_chunks', totalChunks);
|
||||
formData.append('file_size', file.size);
|
||||
formData.append('upload_id', uploadId);
|
||||
formData.append('iso_upload', 'true');
|
||||
formData.append('csrfmiddlewaretoken', form.elements.csrfmiddlewaretoken.value);
|
||||
|
||||
$.ajax({
|
||||
url: form.action,
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(data) {
|
||||
if (chunkIndex === 0) {
|
||||
// the job panel follows the upload from here
|
||||
window.resourceJobPanel.track(data.job);
|
||||
uploadStatusText.text(texts.uploading);
|
||||
uploadDialog.hide();
|
||||
}
|
||||
retryCount = 0;
|
||||
chunkIndex++;
|
||||
pendingUpload.chunkIndex = chunkIndex;
|
||||
|
||||
if (chunkIndex < totalChunks) {
|
||||
uploadChunk();
|
||||
} else {
|
||||
pendingUpload = null;
|
||||
uploadInProgress = false;
|
||||
uploadStatus.removeClass('d-flex').addClass('d-none');
|
||||
uploadButton.prop('disabled', false);
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
if ((jqXHR.status === 0 || jqXHR.status === 429 || jqXHR.status >= 500) && retryCount < 3) {
|
||||
retryCount++;
|
||||
uploadStatusText.text(texts.retrying);
|
||||
setTimeout(uploadChunk, 1000 * Math.pow(2, retryCount - 1));
|
||||
return;
|
||||
}
|
||||
uploadInProgress = false;
|
||||
uploadStatus.removeClass('d-flex').addClass('d-none');
|
||||
uploadButton.prop('disabled', false);
|
||||
if (jqXHR.status >= 400 && jqXHR.status < 500 && jqXHR.status !== 429) {
|
||||
pendingUpload = null;
|
||||
}
|
||||
var errorText = jqXHR.responseJSON && jqXHR.responseJSON.error ? jqXHR.responseJSON.error : texts.failed;
|
||||
errorMessage.text(errorText).removeClass("d-none");
|
||||
uploadDialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
uploadChunk();
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="modal fade" id="AddStgPool" tabindex="-1" aria-labelledby="AddStgPoolLabel" aria-hidden="true"
|
||||
{% if form.errors %}data-open{% endif %}>
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable modal-fullscreen-sm-down">
|
||||
<form method="post" class="modal-content" id="pool-form">{% csrf_token %}
|
||||
<form method="post" class="modal-content" id="pool-form"{% if ceph_secrets %} data-ceph-secrets{% endif %}>{% csrf_token %}
|
||||
<input type="hidden" name="stg_type" value="{{ form.stg_type.value|default:"dir" }}">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
|
|
@ -187,36 +187,5 @@
|
|||
</div>
|
||||
</div>
|
||||
<script src="{% static 'js/choice_dialog.js' %}"></script>
|
||||
<script>
|
||||
// the ISO pool is a directory named iso; a path follows the pool's name
|
||||
// until it is edited (after the page's scripts, Bootstrap's among them)
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var form = document.getElementById('pool-form');
|
||||
var name = form.elements.namedItem('name');
|
||||
var otherName = name.value;
|
||||
var hasCephSecrets = {{ ceph_secrets|yesno:"true,false" }};
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
|
||||
});
|
||||
name.addEventListener('input', function () {
|
||||
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
||||
var derived = name.value ? path.dataset.followName + name.value : '';
|
||||
if (!path.value || path.value === path.dataset.derived) path.value = derived;
|
||||
path.dataset.derived = derived;
|
||||
});
|
||||
});
|
||||
// a new device or name: the erasing is agreed to again
|
||||
form.querySelectorAll('#pool_lvm_source, #pool_name').forEach(function (field) {
|
||||
field.addEventListener('input', function () { document.getElementById('pool_lvm_erase').checked = false; });
|
||||
});
|
||||
choiceDialog(document.getElementById('AddStgPool'), 'pool_kind', function (kind) {
|
||||
var iso = kind === 'iso';
|
||||
if (!name.readOnly) otherName = name.value;
|
||||
name.value = iso ? 'iso' : otherName;
|
||||
name.readOnly = iso;
|
||||
form.elements.namedItem('stg_type').value = iso ? 'dir' : kind;
|
||||
form.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/pool_dialog.js' %}"></script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@
|
|||
<h5 class="modal-title" id="IsoUploadLabel">{% trans "Upload ISO Image" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
||||
</div>
|
||||
<form id="isoUploadForm" enctype="multipart/form-data" method="post" aria-label="{% trans "Upload iso form" %}">{% csrf_token %}
|
||||
<form id="isoUploadForm" action="{% url 'storage' compute.id pool %}" enctype="multipart/form-data" method="post" aria-label="{% trans "Upload iso form" %}"
|
||||
data-no-file="{% trans 'Please select a file to upload.' %}" data-not-iso="{% trans 'Only .iso files can be uploaded.' %}"
|
||||
data-empty="{% trans 'The selected file is empty.' %}" data-starting="{% trans 'Starting upload...' %}"
|
||||
data-uploading="{% trans 'Uploading; the job panel shows its progress. Stay on this page until it ends.' %}"
|
||||
data-retrying="{% trans 'Retrying upload...' %}" data-failed="{% trans 'An error occurred during upload.' %}">{% csrf_token %}
|
||||
<div class="modal-body">
|
||||
<label for="id_file" class="form-label">{% trans "ISO file" %}</label>
|
||||
<input type="file" name="file" id="id_file" class="form-control" accept=".iso" required aria-describedby="id_file_help">
|
||||
|
|
|
|||
|
|
@ -215,151 +215,5 @@
|
|||
<script src="{% static 'js/poll.js' %}"></script>
|
||||
{% job_panel_script job_resource %}
|
||||
{% if volumes %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
|
||||
<script>
|
||||
// the clone dialog: filled for the volume whose button opened it, its
|
||||
// choices reset; the format rows show while converting
|
||||
function showCloneRows() {
|
||||
var convert = $('#clone-convert').prop('checked');
|
||||
$('#clone-format-row').toggleClass('d-none', !convert);
|
||||
$('#clone-prealloc-row').toggleClass('d-none', !(convert && $('#clone-format').val() == 'qcow2'));
|
||||
}
|
||||
$('#CloneVolume').on('show.bs.modal', function (event) {
|
||||
var volume = event.relatedTarget.dataset.volume;
|
||||
$('#clone-image').val(volume);
|
||||
$('#clone-image-name').text(volume);
|
||||
$('#clone-name').val('');
|
||||
$('#clone-convert, #clone-prealloc').prop('checked', false);
|
||||
$('#clone-format').val('raw');
|
||||
showCloneRows();
|
||||
});
|
||||
$('#clone-convert, #clone-format').on('change', showCloneRows);
|
||||
|
||||
// a refused new volume: its dialog opens again on what was entered, at the refused field
|
||||
var addImage = document.getElementById('AddImage');
|
||||
if (addImage && addImage.hasAttribute('data-open')) {
|
||||
addImage.addEventListener('shown.bs.modal', focusFirstError, {once: true});
|
||||
bootstrap.Modal.getOrCreateInstance(addImage).show();
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var pendingUpload = null;
|
||||
var uploadInProgress = false;
|
||||
// the browser sends the file: leaving the page stops the upload
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (uploadInProgress) { e.preventDefault(); e.returnValue = ''; }
|
||||
});
|
||||
$('#isoUploadForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
if (uploadInProgress) return;
|
||||
|
||||
var fileInput = $('#id_file')[0];
|
||||
var errorMessage = $('#upload-error-message');
|
||||
if (fileInput.files.length === 0) {
|
||||
errorMessage.text("{% trans 'Please select a file to upload.'|escapejs %}").removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
var file = fileInput.files[0];
|
||||
var fileName = file.name;
|
||||
if (!/\.iso$/i.test(fileName) || fileName.length <= 4) {
|
||||
errorMessage.text("{% trans 'Only .iso files can be uploaded.'|escapejs %}").removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
if (file.size === 0) {
|
||||
errorMessage.text("{% trans 'The selected file is empty.'|escapejs %}").removeClass("d-none");
|
||||
return;
|
||||
}
|
||||
var chunkSize = 32 * 1024 * 1024; // Fewer SSH connections per upload
|
||||
var totalChunks = Math.ceil(file.size / chunkSize);
|
||||
var fileSignature = [fileName, file.size, file.lastModified].join(':');
|
||||
if (!pendingUpload || pendingUpload.signature !== fileSignature) {
|
||||
pendingUpload = {
|
||||
signature: fileSignature,
|
||||
chunkIndex: 0,
|
||||
uploadId: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : (Date.now() + '-' + Math.random().toString(36).substring(2, 10))
|
||||
};
|
||||
}
|
||||
var chunkIndex = pendingUpload.chunkIndex;
|
||||
var uploadId = pendingUpload.uploadId;
|
||||
var retryCount = 0;
|
||||
uploadInProgress = true;
|
||||
|
||||
var uploadDialog = bootstrap.Modal.getOrCreateInstance(document.getElementById('IsoUpload'));
|
||||
var uploadStatus = $('#upload-status');
|
||||
var uploadStatusText = $('#upload-status-text');
|
||||
var uploadButton = $('#isoUploadForm button[type="submit"]');
|
||||
|
||||
errorMessage.addClass("d-none");
|
||||
uploadStatusText.text("{% trans 'Starting upload...'|escapejs %}");
|
||||
uploadStatus.removeClass('d-none').addClass('d-flex');
|
||||
uploadButton.prop('disabled', true);
|
||||
|
||||
function uploadChunk() {
|
||||
if (chunkIndex >= totalChunks) {
|
||||
return;
|
||||
}
|
||||
|
||||
var start = chunkIndex * chunkSize;
|
||||
var end = Math.min(start + chunkSize, file.size);
|
||||
var chunk = file.slice(start, end);
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('file', chunk, fileName);
|
||||
formData.append('file_name', fileName);
|
||||
formData.append('chunk_index', chunkIndex);
|
||||
formData.append('total_chunks', totalChunks);
|
||||
formData.append('file_size', file.size);
|
||||
formData.append('upload_id', uploadId);
|
||||
formData.append('iso_upload', 'true');
|
||||
formData.append('csrfmiddlewaretoken', '{{ csrf_token }}');
|
||||
|
||||
$.ajax({
|
||||
url: "{% url 'storage' compute.id pool %}",
|
||||
type: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(data) {
|
||||
if (chunkIndex === 0) {
|
||||
// the job panel follows the upload from here
|
||||
window.resourceJobPanel.track(data.job);
|
||||
uploadStatusText.text("{% trans 'Uploading; the job panel shows its progress. Stay on this page until it ends.'|escapejs %}");
|
||||
uploadDialog.hide();
|
||||
}
|
||||
retryCount = 0;
|
||||
chunkIndex++;
|
||||
pendingUpload.chunkIndex = chunkIndex;
|
||||
|
||||
if (chunkIndex < totalChunks) {
|
||||
uploadChunk();
|
||||
} else {
|
||||
pendingUpload = null;
|
||||
uploadInProgress = false;
|
||||
uploadStatus.removeClass('d-flex').addClass('d-none');
|
||||
uploadButton.prop('disabled', false);
|
||||
}
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
if ((jqXHR.status === 0 || jqXHR.status === 429 || jqXHR.status >= 500) && retryCount < 3) {
|
||||
retryCount++;
|
||||
uploadStatusText.text("{% trans 'Retrying upload...'|escapejs %}");
|
||||
setTimeout(uploadChunk, 1000 * Math.pow(2, retryCount - 1));
|
||||
return;
|
||||
}
|
||||
uploadInProgress = false;
|
||||
uploadStatus.removeClass('d-flex').addClass('d-none');
|
||||
uploadButton.prop('disabled', false);
|
||||
if (jqXHR.status >= 400 && jqXHR.status < 500 && jqXHR.status !== 429) {
|
||||
pendingUpload = null;
|
||||
}
|
||||
var errorText = jqXHR.responseJSON && jqXHR.responseJSON.error ? jqXHR.responseJSON.error : "{% trans 'An error occurred during upload.'|escapejs %}";
|
||||
errorMessage.text(errorText).removeClass("d-none");
|
||||
uploadDialog.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
uploadChunk();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{% static 'js/storage_page.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -11,3 +11,21 @@ class AddSecret(forms.Form):
|
|||
value = forms.CharField(required=False, strip=False)
|
||||
ephemeral = forms.BooleanField(required=False)
|
||||
private = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
# what a secret's usage and value are, per usage type, for the page's labels
|
||||
USAGE_TEXTS = {
|
||||
"ceph": {"usage": _("Ceph secret name"),
|
||||
"usageHelp": _("The name a disk or pool names it by, e.g. client.libvirt secret, not the Ceph user; one secret per name."),
|
||||
"value": _("Key"), "valueHelp": _("The key as ceph auth get-key prints it.")},
|
||||
"iscsi": {"usage": _("iSCSI target"), "usageHelp": _("The name a disk or pool names it by, e.g. the target IQN."),
|
||||
"value": _("CHAP password"), "valueHelp": _("The password as it is typed.")},
|
||||
"volume": {"usage": _("Volume path"), "usageHelp": _("The path of the encrypted volume it unlocks."),
|
||||
"value": _("Passphrase"), "valueHelp": _("The passphrase as it is typed.")},
|
||||
"tls": {"usage": _("Name"), "usageHelp": _("A name for it; qemu.conf names it by its UUID."),
|
||||
"value": _("Passphrase"), "valueHelp": _("The passphrase of the TLS key, as it is typed.")},
|
||||
"vtpm": {"usage": _("Name"), "usageHelp": _("A name for it; a VM's vTPM names it by its UUID."),
|
||||
"value": _("Passphrase"), "valueHelp": _("The passphrase as it is typed.")},
|
||||
"none": {"value": _("Value"), "valueHelp": _("The value as it is typed.")},
|
||||
"later": _("Leave it blank to give it later."),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,50 +114,6 @@
|
|||
|
||||
{% block script %}
|
||||
{% if secrets_all %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
|
||||
<script>
|
||||
// what a secret's usage and value are, per usage type
|
||||
var usageTexts = {
|
||||
ceph: {usage: "{{ _('Ceph secret name')|escapejs }}", usageHelp: "{{ _('The name a disk or pool names it by, e.g. client.libvirt secret, not the Ceph user; one secret per name.')|escapejs }}",
|
||||
value: "{{ _('Key')|escapejs }}", valueHelp: "{{ _('The key as ceph auth get-key prints it.')|escapejs }}"},
|
||||
iscsi: {usage: "{{ _('iSCSI target')|escapejs }}", usageHelp: "{{ _('The name a disk or pool names it by, e.g. the target IQN.')|escapejs }}",
|
||||
value: "{{ _('CHAP password')|escapejs }}", valueHelp: "{{ _('The password as it is typed.')|escapejs }}"},
|
||||
volume: {usage: "{{ _('Volume path')|escapejs }}", usageHelp: "{{ _('The path of the encrypted volume it unlocks.')|escapejs }}",
|
||||
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase as it is typed.')|escapejs }}"},
|
||||
tls: {usage: "{{ _('Name')|escapejs }}", usageHelp: "{{ _('A name for it; qemu.conf names it by its UUID.')|escapejs }}",
|
||||
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase of the TLS key, as it is typed.')|escapejs }}"},
|
||||
vtpm: {usage: "{{ _('Name')|escapejs }}", usageHelp: "{{ _("A name for it; a VM's vTPM names it by its UUID.")|escapejs }}",
|
||||
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase as it is typed.')|escapejs }}"},
|
||||
none: {value: "{{ _('Value')|escapejs }}", valueHelp: "{{ _('The value as it is typed.')|escapejs }}"},
|
||||
};
|
||||
|
||||
var usageType = document.getElementById("secret_usage_type");
|
||||
if (usageType) {
|
||||
var showUsage = function () {
|
||||
var texts = usageTexts[usageType.value];
|
||||
document.getElementById("secret_usage_label").textContent = texts.usage;
|
||||
document.getElementById("secret_usage_help").textContent = texts.usageHelp;
|
||||
document.getElementById("secret_value_label").textContent = texts.value;
|
||||
document.getElementById("secret_value_help").textContent = texts.valueHelp + " {{ _('Leave it blank to give it later.')|escapejs }}";
|
||||
};
|
||||
usageType.addEventListener("change", showUsage);
|
||||
showUsage();
|
||||
}
|
||||
|
||||
{% if secrets_all %}
|
||||
document.getElementById("SetSecretValue").addEventListener("show.bs.modal", function (event) {
|
||||
var row = event.relatedTarget.dataset;
|
||||
var texts = usageTexts[row.type] || usageTexts.none;
|
||||
var form = this.querySelector("form");
|
||||
form.reset(); // no value of another secret stays in the field
|
||||
form.elements.uuid.value = row.uuid;
|
||||
document.getElementById("value_usage").textContent = row.usage;
|
||||
document.getElementById("set_value_label").textContent = texts.value;
|
||||
document.getElementById("set_value_help").textContent = texts.valueHelp;
|
||||
// a passphrase's data stays encrypted with the old one
|
||||
var passphrase = ["volume", "tls", "vtpm"].includes(row.type);
|
||||
document.getElementById("set_value_note").hidden = !passphrase;
|
||||
form.elements.value.setAttribute("aria-describedby", passphrase ? "set_value_help set_value_note" : "set_value_help");
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
{{ usage_texts|json_script:"usage-texts" }}
|
||||
<script src="{% static 'js/secrets_page.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from libvirt import libvirtError
|
|||
from logs.audit import audit
|
||||
from vrtManager.virtsecrets import wvmSecrets
|
||||
|
||||
from virtsecrets.forms import AddSecret
|
||||
from virtsecrets.forms import USAGE_TEXTS, AddSecret
|
||||
|
||||
|
||||
@superuser_only
|
||||
|
|
@ -60,4 +60,4 @@ def secrets(request, compute_id):
|
|||
|
||||
usage_choices = AddSecret.base_fields["usage_type"].choices
|
||||
return render(request, "secrets.html", {"secrets_all": secrets_all, "compute": compute,
|
||||
"usage_choices": usage_choices})
|
||||
"usage_choices": usage_choices, "usage_texts": USAGE_TEXTS})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from pathlib import Path
|
|||
from django.conf import settings
|
||||
|
||||
BASE = Path(settings.BASE_DIR)
|
||||
POLLING_SOURCES = ["static/js/instance_status.js", "static/js/instance_stats.js", "computes/templates/overview.html"]
|
||||
POLLING_SOURCES = ["static/js/instance_status.js", "static/js/instance_stats.js", "static/js/host_graphs.js"]
|
||||
|
||||
|
||||
class PollTestCase(unittest.TestCase):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue