Verified Commit 8e647fb1 authored by Štěpán Henek's avatar Štěpán Henek 🌩

subordinates moved to a separate plugin

parent 41cbe6d2
......@@ -31,7 +31,6 @@ from .pages.base import ConfigPageMixin, JoinedPages # TODO refactor plugins an
from .pages.notifications import NotificationsConfigPage
from .pages.password import PasswordConfigPage
from .pages.remote import RemoteConfigPage
from .pages.subordinates import SubordinatesJoinedPage
from .pages.guide import ProfileConfigPage, GuideFinishedPage
from .pages.networks import NetworksConfigPage
from .pages.wan import WanConfigPage
......@@ -51,7 +50,6 @@ config_pages = {
e.slug: e for e in [
NotificationsConfigPage,
RemoteConfigPage,
SubordinatesJoinedPage,
PasswordConfigPage,
ProfileConfigPage,
NetworksConfigPage,
......
This diff is collapsed.
# Foris - web administration interface for OpenWrt
# Copyright (C) 2019 CZ.NIC, z.s.p.o. <http://www.nic.cz>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
import bottle
import typing
from foris import fapi
from foris.form import File, Hidden, Textbox
from foris.state import current_state
from foris.utils.translators import (
gettext_dummy as gettext,
gettext as _,
)
from .base import BaseConfigHandler
from .wifi import WifiEditForm
class SubordinatesConfigHandler(BaseConfigHandler):
def get_form(self):
form = fapi.ForisForm("suboridinates", self.data)
section = form.add_section(
name="main_section",
title=_(self.userfriendly_title),
)
section.add_field(
File, name="token_file", label=_("Token file"), required=True)
def form_cb(data):
res = current_state.backend.perform(
"subordinates", "add_sub",
{"token": base64.b64encode(data["token_file"].file.read()).decode("utf-8")}
)
return "save_result", res
form.add_callback(form_cb)
return form
def get_controller_id_form(self, data=None):
controller_id_form = fapi.ForisForm("controller_id_form", data)
controller_section = controller_id_form.add_section("controller_section", title=None)
controller_section.add_field(
Hidden, name="controller_id", label="", required=True,
)
return controller_id_form
class SubordinatesEditForm(fapi.ForisAjaxForm):
template_name = "config/_subordinates_edit.html.j2"
def __init__(self, data, controller_id=None):
self.subordinate_controller_id = data["controller_id"]
super().__init__(data, controller_id)
self.title = _("Edit device '%(controller_id)s'") % dict(
controller_id=data["controller_id"])
def convert_data_from_backend_to_form(self, backend_data):
subordinates_list = backend_data["subordinates"]
subordinates_map = {e["controller_id"]: e for e in subordinates_list}
sub_record = subordinates_map.get(self.subordinate_controller_id, None)
if not sub_record:
raise bottle.HTTPError(
404, f"Controller id {self.subordinate_controller_id} not found."
)
return sub_record["options"]
def convert_data_from_form_to_backend(self, data):
controller_id = data.pop("controller_id")
return {
"controller_id": controller_id,
"options": data
}
def make_form(self, data: typing.Optional[dict]):
form_data = self.convert_data_from_backend_to_form(
current_state.backend.perform("subordinates", "list")
)
if data:
form_data.update(data)
sub_form = fapi.ForisForm("update_sub", form_data)
sub_section = sub_form.add_section(
"subordinate_section", title="", description=_(
"You can edit managed devices here. These managed devices are directly connected to this "
"device."
)
)
sub_section.add_field(
Textbox, name="custom_name", label=_("Custom Name"),
hint=_("Nicer name for your device '%(controller_id)s'.")
% dict(controller_id=data["controller_id"])
)
sub_section.add_field(
Hidden, name="controller_id", required=True, title="",
)
def form_cb(data):
msg_data = self.convert_data_from_form_to_backend(data)
res = current_state.backend.perform("subordinates", "update_sub", msg_data)
return "save_result", res
sub_form.add_callback(form_cb)
return sub_form
class SubsubordinatesEditForm(fapi.ForisAjaxForm):
template_name = "config/_subordinates_edit.html.j2"
def __init__(self, data, controller_id=None):
self.subsubordinate_controller_id = data["controller_id"]
super().__init__(data, controller_id)
self.title = _("Edit managed device '%(controller_id)s'") % dict(
controller_id=data["controller_id"])
def convert_data_from_backend_to_form(self, backend_data):
subordinates_list = backend_data["subordinates"]
subsubordinates_map = {
e["controller_id"]: e
for record in subordinates_list
for e in record["subsubordinates"]
}
subsub_record = subsubordinates_map.get(self.subsubordinate_controller_id, None)
if not subsub_record:
raise bottle.HTTPError(
404, f"Controller id {self.subsubordinate_controller_id} not found."
)
return subsub_record["options"]
def convert_data_from_form_to_backend(self, data):
controller_id = data.pop("controller_id")
return {
"controller_id": controller_id,
"options": data
}
def make_form(self, data: typing.Optional[dict]):
form_data = self.convert_data_from_backend_to_form(
current_state.backend.perform("subordinates", "list")
)
if data:
form_data.update(data)
sub_form = fapi.ForisForm("update_subsub", form_data)
sub_section = sub_form.add_section(
"subsubordinate_section", title="", description=_(
"You can edit managed devices here. These devices are not "
"not directly connected to this device but "
"they are connected through another managed device."
)
)
sub_section.add_field(
Hidden, name="controller_id", required=True, title="",
)
sub_section.add_field(
Textbox, name="custom_name", label=_("Custom Name"),
hint=_("Nicer name for your device with serial '%(controller_id)s'.")
% dict(controller_id=self.subsubordinate_controller_id)
)
def form_cb(data):
res = current_state.backend.perform(
"subordinates", "update_subsub",
{
"controller_id": data["controller_id"],
"options": {"custom_name": data["custom_name"]}
}
)
return "save_result", res
sub_form.add_callback(form_cb)
return sub_form
class SubordinatesWifiHandler(BaseConfigHandler):
def get_form(self):
ajax_form = WifiEditForm(self.data)
return ajax_form.foris_form
<div id="subordinates-edit" {% if hide %}style="display: none"{% endif %}>
<h3>{{ ajax_form.title }}</h3>
{% include 'config/_message.html.j2' %}
<form id="sub-form" class="config-form" action="{{ ajax_form.url }}" method="post" autocomplete="off" novalidate>
<p class="config-description">{{ form.sections[0].description|safe }}</p>
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<input type="hidden" name="action" value="{{ form.name }}">
{% for field in form.active_fields %}
{% include '_field.html.j2' %}
{% endfor %}
<div id="{{ 'form-%s-buttons' % form.name }}" class="form-buttons">
<a href="#" class="button grayed">{% trans %}Close{% endtrans %}</a>
<button type="submit" name="apply" class="button">{% trans %}Apply{% endtrans %}</button>
</div>
</form>
</div>
{% macro sub_buttons(controller_id, enabled, form_class) -%}
<form action="{{ url("config_ajax", page_name="subordinates") }}" method="post" class="subordinate-buttons {{ form_class }}">
<input type="hidden" name="controller_id" value="{{ controller_id }}">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
{% if enabled %}
<button name="action" value="disable" type="submit"><i class="fas fa-pause"></i></button>
{% else %}
<button name="action" value="enable" type="submit"><i class="fas fa-play"></i></button>
{% endif %}
<button name="action" value="delete" type="submit"><i class="fas fa-trash-alt"></i> </button>
<button name="action" value="edit" type="submit"><i class="fas fa-edit"></i></button>
</form>
{%- endmacro %}
{% macro convert_state(enabled, sup_enabled) -%}
{% if enabled and sup_enabled %}
<i class="fas fa-question-circle state"></i>
{% else %}
<i class="fas fa-pause-circle state"></i>
{% endif %}
{%- endmacro %}
{%- macro should_be_connected_class(enabled, sup_enabled) %}
{% if enabled and sup_enabled %} sub-connection{% endif %}
{%- endmacro %}
{% if subordinates %}
<table id="subordinates-table">
<thead><th>{% trans %}ID{% endtrans %}</th><th>{% trans %}State{% endtrans %}</th><th></th></thead>
<tbody>
{% for sub in subordinates %}
<tr id="sub-{{ sub.controller_id }}" class="sub-line {{ should_be_connected_class(sub.enabled, True) }}">
<td>{{ sub.options.custom_name or sub.controller_id }}</td>
<td>
{{ convert_state(sub.enabled, True) }}
</td>
<td>{{ sub_buttons(sub.controller_id, sub.enabled, "sub-form") }}</td>
</tr>
{% for subsub in sub.subsubordinates %}
<tr id="sub-{{ subsub.controller_id }}" class="sub-line {{ should_be_connected_class(subsub.enabled, sub.enabled) }}">
<td class="indented-item"><i class="fas fa-level-up-alt rotate-90"></i>
{{ subsub.options.custom_name or subsub.controller_id }}
</td>
<td>
{{ convert_state(subsub.enabled, sub.enabled) }}
</td>
<td>{{ sub_buttons(subsub.controller_id, subsub.enabled, "subsub-form") }}</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% else %}
<p id="subordinates-table">{% trans %}Currently there are no managed devices.{% endtrans %}</p>
{% endif %}
{% macro make_simple_edit_form(controller_id, form_name) -%}
<form action="{{ url("config_ajax_form", page_name="subordinates", form_name=form_name) }}" method="post" class="subordinate-buttons">
<input type="hidden" name="controller_id" value="{{ controller_id }}">
<input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">
<button name="action" value="edit" type="submit" style="display: none"><i class="fas fa-edit"></i></button>
</form>
{% endmacro -%}
{% macro convert_state(enabled, sup_enabled) -%}
{% if enabled and sup_enabled %}
<i class="fas fa-question-circle state"></i>
{% else %}
<i class="fas fa-pause-circle state"></i>
{% endif %}
{%- endmacro %}
{%- macro should_be_connected_class(enabled, sup_enabled) %}
{% if enabled and sup_enabled %} sub-connection{% endif %}
{%- endmacro %}
{% if subordinates %}
<table id="subordinates-table">
<thead><th>{% trans %}ID{% endtrans %}</th><th>{% trans %}State{% endtrans %}</th><th>{% trans %}Devices{% endtrans %}</th><th>{% trans %}Channels{% endtrans %}</th><th></th></thead>
<tbody>
{% for sub in subordinates %}
<tr id="sub-{{ sub.controller_id }}" class="sub-line {{ should_be_connected_class(sub.enabled, True) }}">
<td>{{ sub.options.custom_name or sub.controller_id }}</td>
<td>
{{ convert_state(sub.enabled, True) }}
</td>
<td class="wifi-count"><i class="fas fa-spinner rotate"></i></td>
<td class="wifi-channels"><i class="fas fa-spinner rotate"></i></td>
<td>
{{ make_simple_edit_form(sub.controller_id, "wifi") }}
</td>
</tr>
{% for subsub in sub.subsubordinates %}
<tr id="sub-{{ subsub.controller_id }}" class="sub-line {{ should_be_connected_class(subsub.enabled, sub.enabled) }}" >
<td class="indented-item"><i class="fas fa-level-up-alt rotate-90"></i>
{{ subsub.options.custom_name or subsub.controller_id }}
</td>
<td>
{{ convert_state(subsub.enabled, sub.enabled) }}
</td>
<td class="wifi-count"><i class="fas fa-spinner rotate"></i></td>
<td class="wifi-channels"><i class="fas fa-spinner rotate"></i></td>
<td>
{{ make_simple_edit_form(subsub.controller_id, "wifi") }}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% else %}
<p id="subordinates-table">{% trans %}Currently there are no subordinates.{% endtrans %}</p>
{% endif %}
This diff is collapsed.
{% extends 'config/base.html.j2' %}
{% block config_base %}
{% if is_xhr is not defined %}
<div id="page-config" class="config-page">
{% endif %}
<p>
{%- trans %}
On this tab you can set up Wi-Fi of other devices controlled by this device.
{%- endtrans %}
</p>
{% if is_xhr is not defined %}
<h3>{% trans %}Device list{% endtrans %}</h3>
<div id="subordinates-message"></div>
<div id="subordinates-table">
{% trans %}Loading manged devices...{% endtrans %}
</div>
<br />
<div id="subordinates-edit">
</div>
<script>
Foris.queryBackendWifi = async (controller_id) => {
let run = async () => {
try {
resp = await Foris.performBackendQuery(controller_id, "wifi", "get_settings");
$(`#sub-${controller_id}`).find(".wifi-count").text(resp.devices.length);
let channels = [];
for (let device of resp.devices) {
if (device.enabled) {
channels.push(device.channel == 0 ? "*": device.channel);
}
};
$(`#sub-${controller_id}`).find(".wifi-channels").text(channels.join(", "));
} catch(err) {
$(`#sub-${controller_id}`).find(".wifi-count").text("?");
$(`#sub-${controller_id}`).find(".wifi-channels").text("?");
}
};
run(); // call async don't wait for it
};
Foris.loadSubordinatesList = async () => {
let resp = await $.get('{{ url("config_ajax", page_name="subordinates-wifi") }}', {action: "list"});
$("#subordinates-table").replaceWith(resp);
Foris.overrideSubordinatesButtons();
Foris.setSubordinatesTimeouts();
}
Foris.setSubordinatesTimeouts = () => {
for (timeout in Foris.subordinateKeepAliveTimeouts) {
clearTimeout(Foris.subordinateKeepAliveTimeouts[timeout]);
}
Foris.subordinateKeepAliveTimeouts = {};
$(".sub-connection").each((idx, val) => {
let id = $(val).attr("id");
if (id) {
Foris.subordinateSetKeepAliveTimeout(id.replace('sub-', ''));
}
});
};
Foris.overrideSubordinatesEditButton = async () => {
$("#subordinates-edit form").submit(async (e) => {
e.preventDefault();
let form = $(e.currentTarget);
resp = $.ajax({
type: "POST",
url: form.attr('action'),
data: form.serialize()
});
form.find("input, select, button").attr("disabled", "disabled");
let new_form = await resp;
$("#subordinates-edit").replaceWith(new_form);
await Foris.suboridnatesOverrideEditClose();
await Foris.overrideSubordinatesEditButton();
await Foris.loadSubordinatesList();
$(".subordinate-buttons button").hide();
});
};
Foris.overrideSubordinatesButtons = async () => {
$(".subordinate-buttons").submit(async (e) => {
e.preventDefault();
let form = $(e.currentTarget);
let action = $(e.originalEvent.explicitOriginalTarget).val();
let form_data = {};
let serialized = form.serializeArray();
for (record in serialized) {
form_data[serialized[record].name] = serialized[record].value;
}
let controller_id = form_data.controller_id;
let renderResponse = (resp) => {
$("#subordinates-message").replaceWith(resp);
};
let resp = null;
switch (action) {
case "edit":
Foris.subordinateInEditMode = true;
try {
resp = await $.ajax({
type: "POST",
url: '{{ url("config_ajax_form", page_name="subordinates-wifi", form_name="wifi-form") }}',
data: `&csrf_token=${form_data["csrf_token"]}&_controller_id=${form_data["controller_id"]}&_update=1`,
});
} catch(err) {
Foris.subordinateInEditMode = false;
return;
}
await $("#subordinates-edit").promise();
$("#subordinates-edit").replaceWith(resp);
$(".subordinate-buttons button").hide('slow');
await $(".subordinate-buttons button").promise();
$("#subordinates-edit").show('slow');
await $("#subordinates-edit").promise();
await Foris.suboridnatesOverrideEditClose();
Foris.overrideSubordinatesEditButton();
return;
}
$(".subordinate-buttons button").prop('disabled', true);
});
};
Foris.suboridnatesOverrideEditClose = async () => {
$("#subordinates-edit").find("a.button").click(async (e) => {
Foris.subordinateInEditMode = false;
e.preventDefault();
$("#subordinates-edit").hide('slow');
await $("#subordinates-edit").promise();
$("#subordinates-edit").replaceWith("<div id='subordinates-edit'></div>");
if (Foris.subordinatesGetView() == "subordinates") {
$(".subordinate-buttons button").show('slow');
await $(".subordinate-buttons button").promise();
}
});
};
Foris.subordinateInEditMode = false;
Foris.subordinatesAlive = {};
Foris.subordinateKeepAliveTimeouts = {};
Foris.subordinateSetKeepAliveTimeout = (controller_id) => {
let element = $(`#sub-${controller_id}`);
let state_element = element.find(".state");
Foris.subordinateKeepAliveTimeouts[controller_id] = setTimeout(() => {
Foris.subordinatesAlive[controller_id] = false;
state_element.removeClass();
state_element.addClass("state fas fa-exclamation-circle");
delete Foris.subordinateKeepAliveTimeouts[controller_id];
$(`div[data-controller-id=${controller_id}]`).replaceWith(`<div id='subordinates-edit' class='message error' data-controller-id='${controller_id}'>${Foris.messages.subordinatesSubordinatesFailed(controller_id)}</div>`);
Foris.subordinateInEditMode = false;
}, 3000);
};
Foris.updateSubordinateState = async (data) => {
let element = $(`#sub-${data.id}`);
let state_element = element.find(".state");
if (!document.hidden) {
await state_element.animate({opacity: '0.2'}, 50);
}
state_element.removeClass();
switch (data.state) {
case 'started':
state_element.addClass("state fas fa-circle");
break;
case 'running':
if (data.id in Foris.subordinateKeepAliveTimeouts) {
clearTimeout(Foris.subordinateKeepAliveTimeouts[data.id]);
}
state_element.addClass("state fas fa-check-circle");
Foris.subordinateSetKeepAliveTimeout(data.id);
if (!Foris.subordinatesAlive[data.id]) { // query backend if needed
Foris.queryBackendWifi(data.id);
}
Foris.subordinatesAlive[data.id] = true;
if (!Foris.subordinateInEditMode) {
$(`#sub-${data.id}`).find("button[value=edit]").show("slow");
let message = $(`div[data-controller-id=${data.id}].message`);
message.hide("slow");
await message.promise();
message.replaceWith("<div id='subordinates-edit'></div>");
}
break;
case 'exitted':
state_element.addClass("state fas fa-exclamation-circle");
break;
default:
state_element.addClass("state fas fa-question-circle");
break;
}
if (!document.hidden) {
await state_element.animate({opacity: '1.0'}, 50);
}
};
Foris.addWsHanlder("subordinates", async (msg) => {
switch(msg.action) {
case "add_sub":
case "add_subsub":
case "del":
case "set_enabled":
await Foris.loadSubordinatesList();
break;
}
});
Foris.addWsHanlder("remote", async (msg) => {
switch(msg.action) {
case "advertize":
Foris.updateSubordinateState(msg.data);
break;
};
}, '+');
$(document).ready(function() {
Foris.loadSubordinatesList();
Foris.afterAjaxUpdateFunctions.push(Foris.suboridnatesOverrideEditClose);
Foris.afterAjaxUpdateFunctions.push(Foris.overrideSubordinatesEditButton);
});
</script>
<style>
#subordinates-table .sub-line .state {
font-size: 1.5rem;
};
#subordinates-table td {
vertical-align: top;
};
</style>
</div>
{% endif %}
{% endblock %}
......@@ -23,9 +23,6 @@ Foris.messages.networkRestartTriggered = "{% trans %}A network restart was trigg
Foris.messages.tryingToReconnect = "{% trans %}Trying to reconnect to your device.{% endtrans %}";
Foris.messages.vexYes = "{% trans %}Confirm{% endtrans %}";
Foris.messages.vexNo = "{% trans %}Cancel{% endtrans %}";
Foris.messages.subordinatesSubordinatesFailed = (controller_id) => {
return `{% trans controller_id="${controller_id}" %}Connection to '{{ controller_id }}' was interrupted.{% endtrans %}`;
};
Foris.pingPath = "{{ url('ping') }}";
Foris.backendPath = "{{ url('backend-api') }}";
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment