Development
This commit is contained in:
parent
95a89fcc52
commit
85334bceaa
7 changed files with 51 additions and 51 deletions
|
|
@ -5,14 +5,14 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_upstreamdns', __name__)
|
bp = Blueprint('action_dnsserver', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_upstream_dns'
|
_VIEW = '/view/view_dns_server'
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/upstreamdns_cardupstreamdns_save', methods=['POST'])
|
@bp.route('/action/dnsserver_cardupstreamdns_save', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def upstreamdns_cardupstreamdns_save():
|
def dnsserver_cardupstreamdns_save():
|
||||||
strict_order = 'strict_order' in request.form
|
strict_order = 'strict_order' in request.form
|
||||||
submitted = request.form.getlist('upstream_servers')
|
submitted = request.form.getlist('upstream_servers')
|
||||||
|
|
||||||
|
|
@ -59,9 +59,9 @@ def upstreamdns_cardupstreamdns_save():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/upstreamdns_cardforwardingdnsservice_save', methods=['POST'])
|
@bp.route('/action/dnsserver_carddnsforwarding_save', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def upstreamdns_cardforwardingdnsservice_save():
|
def dnsserver_carddnsforwarding_save():
|
||||||
cache_size = validate.int_range(request.form.get('cache_size', '').strip(), 0, None)
|
cache_size = validate.int_range(request.form.get('cache_size', '').strip(), 0, None)
|
||||||
if cache_size is None:
|
if cache_size is None:
|
||||||
flash('Cache Size must be a non-negative integer.', 'error')
|
flash('Cache Size must be a non-negative integer.', 'error')
|
||||||
|
|
@ -6,9 +6,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_vlans', __name__)
|
bp = Blueprint('action_networklayout', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_vlans'
|
VIEW = '/view/view_network_layout'
|
||||||
|
|
||||||
_VLAN_FIELDS = ['name', 'is_vpn', 'subnet', 'subnet_mask', 'dnsmasq_log_queries',
|
_VLAN_FIELDS = ['name', 'is_vpn', 'subnet', 'subnet_mask', 'dnsmasq_log_queries',
|
||||||
'radius_default', 'mdns_reflection', 'use_blocklists']
|
'radius_default', 'mdns_reflection', 'use_blocklists']
|
||||||
|
|
@ -28,9 +28,9 @@ def _hash_ok():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/add_vlan', methods=['POST'])
|
@bp.route('/action/networklayout_cardaddvlan_addvlan', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def add_vlan():
|
def networklayout_cardaddvlan_addvlan():
|
||||||
name = sanitize.name(request.form.get('name', ''))
|
name = sanitize.name(request.form.get('name', ''))
|
||||||
is_vpn = 'is_vpn' in request.form
|
is_vpn = 'is_vpn' in request.form
|
||||||
subnet = sanitize.ip(request.form.get('subnet', ''))
|
subnet = sanitize.ip(request.form.get('subnet', ''))
|
||||||
|
|
@ -101,9 +101,9 @@ def add_vlan():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/edit_vlan', methods=['POST'])
|
@bp.route('/action/networklayout_tablevlans_edit', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def edit_vlan():
|
def networklayout_tablevlans_edit():
|
||||||
idx = _row_index()
|
idx = _row_index()
|
||||||
if idx is None:
|
if idx is None:
|
||||||
flash('Invalid request.', 'error')
|
flash('Invalid request.', 'error')
|
||||||
|
|
@ -194,9 +194,9 @@ def edit_vlan():
|
||||||
return redirect(VIEW)
|
return redirect(VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/delete_vlan', methods=['POST'])
|
@bp.route('/action/networklayout_tablevlans_delete', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def delete_vlan():
|
def networklayout_tablevlans_delete():
|
||||||
idx = _row_index()
|
idx = _row_index()
|
||||||
if idx is None:
|
if idx is None:
|
||||||
flash('Invalid request.', 'error')
|
flash('Invalid request.', 'error')
|
||||||
|
|
@ -7,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_networkinterfaces', __name__)
|
bp = Blueprint('action_physicalinterfaces', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_network_interfaces'
|
_VIEW = '/view/view_physical_interfaces'
|
||||||
|
|
||||||
_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth',
|
_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth',
|
||||||
'tun', 'tap', 'ppp', 'virbr',
|
'tun', 'tap', 'ppp', 'virbr',
|
||||||
|
|
@ -31,9 +31,9 @@ def _valid_interface(name):
|
||||||
return name in _get_system_interfaces()
|
return name in _get_system_interfaces()
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/networkinterfaces_cardnetworkinterface_save', methods=['POST'])
|
@bp.route('/action/physicalinterfaces_cardphysicalinterface_save', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def networkinterfaces_cardnetworkinterface_save():
|
def physicalinterfaces_cardphysicalinterface_save():
|
||||||
wan = sanitize.interface_name(request.form.get('wan_interface', ''))
|
wan = sanitize.interface_name(request.form.get('wan_interface', ''))
|
||||||
lan = sanitize.interface_name(request.form.get('lan_interface', ''))
|
lan = sanitize.interface_name(request.form.get('lan_interface', ''))
|
||||||
|
|
||||||
|
|
@ -74,9 +74,9 @@ def networkinterfaces_cardnetworkinterface_save():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/networkinterfaces_cardinterfaceconfiguration_apply', methods=['POST'])
|
@bp.route('/action/physicalinterfaces_cardinterfaceconfiguration_apply', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def networkinterfaces_cardinterfaceconfiguration_apply():
|
def physicalinterfaces_cardinterfaceconfiguration_apply():
|
||||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
@ -2,14 +2,14 @@ import os, json, sys
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from view_page import bp as view_page_bp
|
from view_page import bp as view_page_bp
|
||||||
from action_actions import bp as action_actions_bp
|
from action_actions import bp as action_actions_bp
|
||||||
from action_networkinterfaces import bp as action_networkinterfaces_bp
|
from action_physicalinterfaces import bp as action_physicalinterfaces_bp
|
||||||
from action_upstreamdns import bp as action_upstreamdns_bp
|
from action_dnsserver import bp as action_dnsserver_bp
|
||||||
from action_apply_mdns import bp as action_apply_mdns_bp
|
from action_apply_mdns import bp as action_apply_mdns_bp
|
||||||
from action_apply_vpn import bp as action_apply_vpn_bp
|
from action_apply_vpn import bp as action_apply_vpn_bp
|
||||||
from action_apply_banned_ips import bp as action_apply_banned_ips_bp
|
from action_apply_banned_ips import bp as action_apply_banned_ips_bp
|
||||||
from action_apply_host_overrides import bp as action_apply_host_overrides_bp
|
from action_apply_host_overrides import bp as action_apply_host_overrides_bp
|
||||||
from action_dnsblocking import bp as action_dnsblocking_bp
|
from action_dnsblocking import bp as action_dnsblocking_bp
|
||||||
from action_apply_vlans import bp as action_apply_vlans_bp
|
from action_networklayout import bp as action_networklayout_bp
|
||||||
from action_apply_inter_vlan import bp as action_apply_inter_vlan_bp
|
from action_apply_inter_vlan import bp as action_apply_inter_vlan_bp
|
||||||
from action_apply_port_forwarding import bp as action_apply_port_forwarding_bp
|
from action_apply_port_forwarding import bp as action_apply_port_forwarding_bp
|
||||||
from action_apply_dhcp_reservations import bp as action_apply_dhcp_reservations_bp
|
from action_apply_dhcp_reservations import bp as action_apply_dhcp_reservations_bp
|
||||||
|
|
@ -28,14 +28,14 @@ app = Flask(__name__)
|
||||||
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
||||||
app.register_blueprint(view_page_bp)
|
app.register_blueprint(view_page_bp)
|
||||||
app.register_blueprint(action_actions_bp)
|
app.register_blueprint(action_actions_bp)
|
||||||
app.register_blueprint(action_networkinterfaces_bp)
|
app.register_blueprint(action_physicalinterfaces_bp)
|
||||||
app.register_blueprint(action_upstreamdns_bp)
|
app.register_blueprint(action_dnsserver_bp)
|
||||||
app.register_blueprint(action_apply_mdns_bp)
|
app.register_blueprint(action_apply_mdns_bp)
|
||||||
app.register_blueprint(action_apply_vpn_bp)
|
app.register_blueprint(action_apply_vpn_bp)
|
||||||
app.register_blueprint(action_apply_banned_ips_bp)
|
app.register_blueprint(action_apply_banned_ips_bp)
|
||||||
app.register_blueprint(action_apply_host_overrides_bp)
|
app.register_blueprint(action_apply_host_overrides_bp)
|
||||||
app.register_blueprint(action_dnsblocking_bp)
|
app.register_blueprint(action_dnsblocking_bp)
|
||||||
app.register_blueprint(action_apply_vlans_bp)
|
app.register_blueprint(action_networklayout_bp)
|
||||||
app.register_blueprint(action_apply_inter_vlan_bp)
|
app.register_blueprint(action_apply_inter_vlan_bp)
|
||||||
app.register_blueprint(action_apply_port_forwarding_bp)
|
app.register_blueprint(action_apply_port_forwarding_bp)
|
||||||
app.register_blueprint(action_apply_dhcp_reservations_bp)
|
app.register_blueprint(action_apply_dhcp_reservations_bp)
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,14 @@
|
||||||
"label": "%MENU_LABEL%",
|
"label": "%MENU_LABEL%",
|
||||||
"client_requirement": "client_is_viewer+",
|
"client_requirement": "client_is_viewer+",
|
||||||
"items": [
|
"items": [
|
||||||
{ "type": "nav_item", "label": "Network Interfaces", "map_to": "view_network_interfaces", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Physical Interfaces", "map_to": "view_physical_interfaces", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DNS", "map_to": "view_upstream_dns", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "DNS Server", "map_to": "view_dns_server", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DNS Blocking", "map_to": "view_dns_blocking", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "DNS Blocking", "map_to": "view_dns_blocking", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" },
|
{ "type": "nav_item", "label": "Network Layout", "map_to": "view_network_layout", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "VLANs", "map_to": "view_vlans", "client_requirement": "client_is_administrator+" },
|
|
||||||
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "view_inter_vlan", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "view_inter_vlan", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "view_port_forwarding", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "view_port_forwarding", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" },
|
{ "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" },
|
||||||
|
{ "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" },
|
||||||
{ "type": "nav_item", "label": "Host Overrides", "map_to": "view_host_overrides", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Host Overrides", "map_to": "view_host_overrides", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "VPN", "map_to": "view_vpn" },
|
{ "type": "nav_item", "label": "VPN", "map_to": "view_vpn" },
|
||||||
{ "type": "nav_item", "label": "Banned IPs", "map_to": "view_banned_ips", "client_requirement": "client_is_administrator+" }
|
{ "type": "nav_item", "label": "Banned IPs", "map_to": "view_banned_ips", "client_requirement": "client_is_administrator+" }
|
||||||
|
|
|
||||||
|
|
@ -697,7 +697,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "view_network_interfaces",
|
"id": "view_physical_interfaces",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -705,7 +705,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "h1",
|
"type": "h1",
|
||||||
"text": "Network Interfaces"
|
"text": "Physical Interfaces"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "p",
|
"type": "p",
|
||||||
|
|
@ -715,12 +715,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "card",
|
"type": "card",
|
||||||
"label": "Network Interfaces",
|
"label": "Physical Interfaces",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/networkinterfaces_cardnetworkinterface_save",
|
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -744,7 +744,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/networkinterfaces_cardnetworkinterface_save",
|
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Save"
|
"text": "Save"
|
||||||
},
|
},
|
||||||
|
|
@ -767,7 +767,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/networkinterfaces_cardinterfaceconfiguration_apply",
|
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -840,7 +840,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/networkinterfaces_cardinterfaceconfiguration_apply",
|
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Apply"
|
"text": "Apply"
|
||||||
},
|
},
|
||||||
|
|
@ -863,7 +863,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "view_upstream_dns",
|
"id": "view_dns_server",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -871,7 +871,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "h1",
|
"type": "h1",
|
||||||
"text": "DNS"
|
"text": "DNS Server"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "p",
|
"type": "p",
|
||||||
|
|
@ -886,7 +886,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/upstreamdns_cardupstreamdns_save",
|
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -912,7 +912,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/upstreamdns_cardupstreamdns_save",
|
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Save"
|
"text": "Save"
|
||||||
},
|
},
|
||||||
|
|
@ -928,12 +928,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "card",
|
"type": "card",
|
||||||
"label": "Forwarding DNS Service",
|
"label": "DNS Forwarding",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/upstreamdns_cardforwardingdnsservice_save",
|
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -950,7 +950,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/upstreamdns_cardforwardingdnsservice_save",
|
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Save"
|
"text": "Save"
|
||||||
},
|
},
|
||||||
|
|
@ -1470,7 +1470,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "view_vlans",
|
"id": "view_network_layout",
|
||||||
"client_requirement": "client_is_viewer+",
|
"client_requirement": "client_is_viewer+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -1478,7 +1478,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "h1",
|
"type": "h1",
|
||||||
"text": "VLANs"
|
"text": "Network Layout"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "p",
|
"type": "p",
|
||||||
|
|
@ -1549,7 +1549,7 @@
|
||||||
"row_actions": [
|
"row_actions": [
|
||||||
{
|
{
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"action": "/action/edit_vlan",
|
"action": "/action/networklayout_tablevlans_edit",
|
||||||
"method": "inline_edit",
|
"method": "inline_edit",
|
||||||
"text": "Edit",
|
"text": "Edit",
|
||||||
"class": "btn-ghost btn-sm",
|
"class": "btn-ghost btn-sm",
|
||||||
|
|
@ -1593,7 +1593,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"action": "/action/delete_vlan",
|
"action": "/action/networklayout_tablevlans_delete",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Delete",
|
"text": "Delete",
|
||||||
"class": "btn-danger btn-sm",
|
"class": "btn-danger btn-sm",
|
||||||
|
|
@ -1612,7 +1612,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "form",
|
"type": "form",
|
||||||
"action": "/action/add_vlan",
|
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -1708,7 +1708,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"action": "/action/add_vlan",
|
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||||
"method": "post",
|
"method": "post",
|
||||||
"text": "Add VLAN",
|
"text": "Add VLAN",
|
||||||
"class": "add-vlan-btn",
|
"class": "add-vlan-btn",
|
||||||
|
|
|
||||||
|
|
@ -793,4 +793,4 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue