summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-mod-network/htdocs/luci-static/resources
diff options
context:
space:
mode:
authorJo-Philipp Wich <jo@mein.io>2019-09-12 11:06:38 +0200
committerJo-Philipp Wich <jo@mein.io>2019-09-12 11:09:57 +0200
commit82743b3bd4b4603f2c6b85566d07182cb0f1ce52 (patch)
tree9444cba040a334821e6593c13541792c718084ea /modules/luci-mod-network/htdocs/luci-static/resources
parent5fd21bc9efedb29ff8376f8043ed2a4b74788d6b (diff)
luci-mod-network: reimplement switch configuration as client side view
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Diffstat (limited to 'modules/luci-mod-network/htdocs/luci-static/resources')
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js369
1 files changed, 369 insertions, 0 deletions
diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js
new file mode 100644
index 0000000000..b281bb1808
--- /dev/null
+++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/switch.js
@@ -0,0 +1,369 @@
+'use strict';
+'require rpc';
+'require uci';
+'require form';
+'require network';
+
+function parse_portvalue(section_id) {
+ var ports = L.toArray(uci.get('network', section_id, 'ports'));
+
+ for (var i = 0; i < ports.length; i++) {
+ var m = ports[i].match(/^(\d+)([tu]?)/);
+
+ if (m && m[1] == this.option)
+ return m[2] || 'u';
+ }
+
+ return '';
+}
+
+function validate_portvalue(section_id, value) {
+ if (value != 'u')
+ return true;
+
+ var sections = this.section.cfgsections();
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i] == section_id)
+ continue;
+
+ if (this.formvalue(sections[i]) == 'u')
+ return _('%s is untagged in multiple VLANs!').format(this.title);
+ }
+
+ return true;
+}
+
+function update_interfaces(old_ifname, new_ifname) {
+ var interfaces = uci.sections('network', 'interface');
+
+ for (var i = 0; i < interfaces.length; i++) {
+ var old_ifnames = L.toArray(interfaces[i].ifname),
+ new_ifnames = [],
+ changed = false;
+
+ for (var j = 0; j < old_ifnames.length; j++) {
+ if (old_ifnames[j] == old_ifname) {
+ new_ifnames.push(new_ifname);
+ changed = true;
+ }
+ else {
+ new_ifnames.push(old_ifnames[j]);
+ }
+ }
+
+ if (changed) {
+ uci.set('network', interfaces[i]['.name'], 'ifname', new_ifnames.join(' '));
+
+ L.ui.addNotification(null, E('p', _('Interface %q device auto-migrated from %q to %q.')
+ .replace(/%q/g, '"%s"').format(interfaces[i]['.name'], old_ifname, new_ifname)));
+ }
+ }
+}
+
+function render_port_status(node, portstate) {
+ if (!node)
+ return null;
+
+ if (!portstate.link)
+ L.dom.content(node, [
+ E('img', { src: L.resource('icons/port_down.png') }),
+ E('br'),
+ _('no link')
+ ]);
+ else
+ L.dom.content(node, [
+ E('img', { src: L.resource('icons/port_up.png') }),
+ E('br'),
+ '%d'.format(portstate.speed) + _('baseT'),
+ E('br'),
+ portstate.duplex ? _('full-duplex') : _('half-duplex')
+ ]);
+
+ return node;
+}
+
+function update_port_status(topologies) {
+ var tasks = [];
+
+ for (var switch_name in topologies)
+ tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(switch_name, ports) {
+ for (var i = 0; i < ports.length; i++) {
+ var node = document.querySelector('[data-switch="%s"][data-port="%d"]'.format(switch_name, ports[i].port));
+ render_port_status(node, ports[i]);
+ }
+ }, topologies[switch_name], switch_name)));
+
+ return Promise.all(tasks);
+}
+
+var callSwconfigFeatures = rpc.declare({
+ object: 'luci',
+ method: 'getSwconfigFeatures',
+ params: [ 'switch' ],
+ expect: { '': {} }
+});
+
+var callSwconfigPortState = rpc.declare({
+ object: 'luci',
+ method: 'getSwconfigPortState',
+ params: [ 'switch' ],
+ expect: { result: [] }
+});
+
+return L.view.extend({
+ load: function() {
+ return network.getSwitchTopologies().then(function(topologies) {
+ var tasks = [];
+
+ for (var switch_name in topologies) {
+ tasks.push(callSwconfigFeatures(switch_name).then(L.bind(function(features) {
+ this.features = features;
+ }, topologies[switch_name])));
+ tasks.push(callSwconfigPortState(switch_name).then(L.bind(function(ports) {
+ this.portstate = ports;
+ }, topologies[switch_name])));
+ }
+
+ return Promise.all(tasks).then(function() { return topologies });
+ });
+ },
+
+ render: function(topologies) {
+ var m, s, o;
+
+ m = new form.Map('network', _('Switch'), _('The network ports on this device can be combined to several <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s in which computers can communicate directly with each other. <abbr title=\"Virtual Local Area Network\">VLAN</abbr>s are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network.'));
+
+ var switchSections = uci.sections('network', 'switch');
+
+ for (var i = 0; i < switchSections.length; i++) {
+ var switchSection = switchSections[i],
+ sid = switchSection['.name'],
+ switch_name = switchSection.name || sid,
+ topology = topologies[switch_name];
+
+ if (!topology) {
+ L.ui.addNotification(null, _('Switch %q has an unknown topology - the VLAN settings might not be accurate.').replace(/%q/, switch_name));
+
+ topology = {
+ features: {},
+ netdevs: {
+ 5: 'eth0'
+ },
+ ports: [
+ { num: 0, label: 'Port 1' },
+ { num: 1, label: 'Port 2' },
+ { num: 2, label: 'Port 3' },
+ { num: 3, label: 'Port 4' },
+ { num: 4, label: 'Port 5' },
+ { num: 5, label: 'CPU (eth0)', device: 'eth0', need_tag: false }
+ ]
+ };
+ }
+
+ var feat = topology.features,
+ min_vid = feat.min_vid || 0,
+ max_vid = feat.max_vid || 16,
+ num_vlans = feat.num_vlans || 16,
+ switch_title = _('Switch %q').replace(/%q/, '"%s"'.format(switch_name)),
+ vlan_title = _('VLANs on %q').replace(/%q/, '"%s"'.format(switch_name));
+
+ if (feat.switch_title) {
+ switch_title += ' (%s)'.format(feat.switch_title);
+ vlan_title += ' (%s)'.format(feat.switch_title);
+ }
+
+ s = m.section(form.NamedSection, sid, 'switch', switch_title);
+ s.addremove = false;
+
+ if (feat.vlan_option)
+ s.option(form.Flag, feat.vlan_option, _('Enable VLAN functionality'));
+
+ if (feat.learning_option) {
+ o = s.option(form.Flag, feat.learning_option, _('Enable learning and aging'));
+ o.default = o.enabled;
+ }
+
+ if (feat.jumbo_option) {
+ o = s.option(form.Flag, feat.jumbo_option, _('Enable Jumbo Frame passthrough'));
+ o.enabled = '3';
+ o.rmempty = true;
+ }
+
+ if (feat.mirror_option) {
+ s.option(form.Flag, 'enable_mirror_rx', _('Enable mirroring of incoming packets'));
+ s.option(form.Flag, 'enable_mirror_tx', _('Enable mirroring of outgoing packets'));
+
+ var sp = s.option(form.ListValue, 'mirror_source_port', _('Mirror source port')),
+ mp = s.option(form.ListValue, 'mirror_monitor_port', _('Mirror monitor port'));
+
+ sp.depends('enable_mirror_rx', '1');
+ sp.depends('enable_mirror_tx', '1');
+
+ mp.depends('enable_mirror_rx', '1');
+ mp.depends('enable_mirror_tx', '1');
+
+ for (var j = 0; j < topology.ports.length; j++) {
+ sp.value(topology.ports[j].num, topology.ports[j].label);
+ mp.value(topology.ports[j].num, topology.ports[j].label);
+ }
+ }
+
+ s = m.section(form.TableSection, 'switch_vlan', vlan_title);
+ s.anonymous = true;
+ s.addremove = true;
+ s.addbtntitle = _('Add VLAN');
+ s.topology = topology;
+ s.device = switch_name;
+
+ s.filter = function(section_id) {
+ var device = uci.get('network', section_id, 'device');
+ return (device == switch_name);
+ };
+
+ s.cfgsections = function() {
+ var sections = form.TableSection.prototype.cfgsections.apply(this);
+
+ return sections.sort(function(a, b) {
+ var vidA = feat.vid_option ? uci.get('network', a, feat.vid_option) : null,
+ vidB = feat.vid_option ? uci.get('network', b, feat.vid_option) : null;
+
+ vidA = +(vidA != null ? vidA : uci.get('network', a, 'vlan') || 9999);
+ vidB = +(vidB != null ? vidB : uci.get('network', b, 'vlan') || 9999);
+
+ return (vidA - vidB);
+ });
+ };
+
+ s.handleAdd = function(ev) {
+ var sections = uci.sections('network', 'switch_vlan'),
+ section_id = uci.add('network', 'switch_vlan'),
+ max_vlan = 0,
+ max_vid = 0;
+
+ for (var j = 0; j < sections.length; j++) {
+ if (sections[j].device != s.device)
+ continue;
+
+ var vlan = +sections[j].vlan,
+ vid = feat.vid_option ? +sections[j][feat.vid_option] : null;
+
+ if (vlan > max_vlan)
+ max_vlan = vlan;
+
+ if (vid > max_vid)
+ max_vid = vid;
+ }
+
+ uci.set('network', section_id, 'device', s.device);
+ uci.set('network', section_id, 'vlan', max_vlan + 1);
+
+ if (feat.vid_option)
+ uci.set('network', section_id, feat.vid_option, max_vid + 1);
+
+ return this.map.save(null, true);
+ };
+
+ var port_opts = [];
+
+ o = s.option(form.Value, feat.vid_option || 'vlan', 'VLAN ID');
+ o.rmempty = false;
+ o.forcewrite = true;
+ o.vlan_used = {};
+ o.datatype = 'range(%u,%u)'.format(min_vid, feat.vid_option ? 4094 : num_vlans - 1);
+ o.description = _('Port status:');
+
+ o.validate = function(section_id, value) {
+ var v = +value,
+ m = feat.vid_option ? 4094 : num_vlans - 1;
+
+ if (isNaN(v) || v < min_vid || v > m)
+ return _('Invalid VLAN ID given! Only IDs between %d and %d are allowed.').format(min_vid, m);
+
+ var sections = this.section.cfgsections();
+
+ for (var i = 0; i < sections.length; i++) {
+ if (sections[i] == section_id)
+ continue;
+
+ if (this.formvalue(sections[i]) == v)
+ return _('Invalid VLAN ID given! Only unique IDs are allowed');
+ }
+
+ return true;
+ };
+
+ o.write = function(section_id, value) {
+ var topology = this.section.topology,
+ values = [];
+
+ for (var i = 0; i < port_opts.length; i++) {
+ var tagging = port_opts[i].formvalue(section_id),
+ portspec = Array.isArray(topology.ports) ? topology.ports[i] : null;
+
+ if (tagging == 't')
+ values.push(port_opts[i].option + tagging);
+ else if (tagging == 'u')
+ values.push(port_opts[i].option);
+
+ if (portspec && portspec.device) {
+ var old_tag = port_opts[i].cfgvalue(section_id),
+ old_vid = this.cfgvalue(section_id);
+
+ if (old_tag != tagging || old_vid != value) {
+ var old_ifname = portspec.device + (old_tag != 'u' ? '.' + old_vid : ''),
+ new_ifname = portspec.device + (tagging != 'u' ? '.' + value : '');
+
+ if (old_ifname != new_ifname)
+ update_interfaces(old_ifname, new_ifname);
+ }
+ }
+ }
+
+ if (feat.vlan4k_option)
+ uci.set('network', sid, feat.vlan4k_option, '1');
+
+ uci.set('network', section_id, 'ports', values.join(' '));
+
+ return form.Value.prototype.write.apply(this, [section_id, value]);
+ };
+
+ o.cfgvalue = function(section_id) {
+ var value = feat.vid_option ? uci.get('network', section_id, feat.vid_option) : null;
+ return (value || uci.get('network', section_id, 'vlan'));
+ };
+
+ for (var j = 0; Array.isArray(topology.ports) && j < topology.ports.length; j++) {
+ var portspec = topology.ports[j],
+ portstate = Array.isArray(topology.portstate) ? topology.portstate[portspec.num] : null;
+
+ o = s.option(form.ListValue, String(portspec.num), portspec.label);
+ o.value('', _('off'));
+
+ if (!portspec.need_tag)
+ o.value('u', _('untagged'));
+
+ o.value('t', _('tagged'));
+
+ o.cfgvalue = parse_portvalue;
+ o.validate = validate_portvalue;
+ o.write = function() {};
+
+ o.description = render_port_status(E('small', {
+ 'data-switch': switch_name,
+ 'data-port': portspec.num
+ }), portstate);
+
+ port_opts.push(o);
+ }
+
+ port_opts.sort(function(a, b) {
+ return a.option < b.option;
+ });
+ }
+
+ L.Poll.add(L.bind(update_port_status, m, topologies));
+
+ return m.render();
+ }
+});