luci2: implement LuCI2.session.updateACLs() and LuCI2.session.hasACL()
[project/luci2/ui.git] / luci2 / htdocs / luci2 / luci2.js
index 98cd441..58d38df 100644 (file)
@@ -214,7 +214,7 @@ function LuCI2()
                _class.prototype = prototype;
                _class.prototype.constructor = _class;
 
-               _class.extend = arguments.callee;
+               _class.extend = Class.extend;
 
                return _class;
        };
@@ -364,8 +364,10 @@ function LuCI2()
                        h += keys[i] + ':' + data[keys[i]];
                }
 
-               if (h)
+               if (h.length)
                        location.hash = '#' + h;
+               else
+                       location.hash = '';
        };
 
        this.getHash = function(key)
@@ -386,6 +388,103 @@ function LuCI2()
                return data;
        };
 
+       this.toArray = function(x)
+       {
+               switch (typeof(x))
+               {
+               case 'number':
+               case 'boolean':
+                       return [ x ];
+
+               case 'string':
+                       var r = [ ];
+                       var l = x.split(/\s+/);
+                       for (var i = 0; i < l.length; i++)
+                               if (l[i].length > 0)
+                                       r.push(l[i]);
+                       return r;
+
+               case 'object':
+                       if ($.isArray(x))
+                       {
+                               var r = [ ];
+                               for (var i = 0; i < x.length; i++)
+                                       r.push(x[i]);
+                               return r;
+                       }
+                       else if ($.isPlainObject(x))
+                       {
+                               var r = [ ];
+                               for (var k in x)
+                                       if (x.hasOwnProperty(k))
+                                               r.push(k);
+                               return r.sort();
+                       }
+               }
+
+               return [ ];
+       };
+
+       this.toObject = function(x)
+       {
+               switch (typeof(x))
+               {
+               case 'number':
+               case 'boolean':
+                       return { x: true };
+
+               case 'string':
+                       var r = { };
+                       var l = x.split(/\x+/);
+                       for (var i = 0; i < l.length; i++)
+                               if (l[i].length > 0)
+                                       r[l[i]] = true;
+                       return r;
+
+               case 'object':
+                       if ($.isArray(x))
+                       {
+                               var r = { };
+                               for (var i = 0; i < x.length; i++)
+                                       r[x[i]] = true;
+                               return r;
+                       }
+                       else if ($.isPlainObject(x))
+                       {
+                               return x;
+                       }
+               }
+
+               return { };
+       };
+
+       this.filterArray = function(array, item)
+       {
+               if (!$.isArray(array))
+                       return [ ];
+
+               for (var i = 0; i < array.length; i++)
+                       if (array[i] === item)
+                               array.splice(i--, 1);
+
+               return array;
+       };
+
+       this.toClassName = function(str, suffix)
+       {
+               var n = '';
+               var l = str.split(/[\/.]/);
+
+               for (var i = 0; i < l.length; i++)
+                       if (l[i].length > 0)
+                               n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
+
+               if (typeof(suffix) == 'string')
+                       n += suffix;
+
+               return n;
+       };
+
        this.globals = {
                timeout:  15000,
                resource: '/luci2',
@@ -406,43 +505,48 @@ function LuCI2()
                                data:        JSON.stringify(req),
                                dataType:    'json',
                                type:        'POST',
-                               timeout:     _luci2.globals.timeout
-                       }).then(cb);
+                               timeout:     _luci2.globals.timeout,
+                               _rpc_req:   req
+                       }).then(cb, cb);
                },
 
                _list_cb: function(msg)
                {
+                       var list = msg.result;
+
                        /* verify message frame */
-                       if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id)
-                               throw 'Invalid JSON response';
+                       if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
+                               list = [ ];
 
-                       return msg.result;
+                       return $.Deferred().resolveWith(this, [ list ]);
                },
 
                _call_cb: function(msg)
                {
                        var data = [ ];
                        var type = Object.prototype.toString;
+                       var reqs = this._rpc_req;
 
-                       if (!$.isArray(msg))
+                       if (!$.isArray(reqs))
+                       {
                                msg = [ msg ];
+                               reqs = [ reqs ];
+                       }
 
                        for (var i = 0; i < msg.length; i++)
                        {
-                               /* verify message frame */
-                               if (typeof(msg[i]) != 'object' || msg[i].jsonrpc != '2.0' || !msg[i].id)
-                                       throw 'Invalid JSON response';
-
                                /* fetch related request info */
-                               var req = _luci2.rpc._requests[msg[i].id];
+                               var req = _luci2.rpc._requests[reqs[i].id];
                                if (typeof(req) != 'object')
                                        throw 'No related request for JSON response';
 
                                /* fetch response attribute and verify returned type */
                                var ret = undefined;
 
-                               if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
-                                       ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
+                               /* verify message frame */
+                               if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0')
+                                       if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
+                                               ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
 
                                if (req.expect)
                                {
@@ -451,7 +555,7 @@ function LuCI2()
                                                if (typeof(ret) != 'undefined' && key != '')
                                                        ret = ret[key];
 
-                                               if (type.call(ret) != type.call(req.expect[key]))
+                                               if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
                                                        ret = req.expect[key];
 
                                                break;
@@ -473,10 +577,10 @@ function LuCI2()
                                        data = ret;
 
                                /* delete request object */
-                               delete _luci2.rpc._requests[msg[i].id];
+                               delete _luci2.rpc._requests[reqs[i].id];
                        }
 
-                       return data;
+                       return $.Deferred().resolveWith(this, [ data ]);
                },
 
                list: function()
@@ -565,6 +669,481 @@ function LuCI2()
                }
        };
 
+       this.UCIContext = Class.extend({
+
+               init: function()
+               {
+                       this.state = {
+                               newid:   0,
+                               values:  { },
+                               creates: { },
+                               changes: { },
+                               deletes: { },
+                               reorder: { }
+                       };
+               },
+
+               _load: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'get',
+                       params: [ 'config' ],
+                       expect: { values: { } }
+               }),
+
+               _order: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'order',
+                       params: [ 'config', 'sections' ]
+               }),
+
+               _add: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'add',
+                       params: [ 'config', 'type', 'name', 'values' ],
+                       expect: { section: '' }
+               }),
+
+               _set: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'set',
+                       params: [ 'config', 'section', 'values' ]
+               }),
+
+               _delete: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'delete',
+                       params: [ 'config', 'section', 'options' ]
+               }),
+
+               load: function(packages)
+               {
+                       var self = this;
+                       var seen = { };
+                       var pkgs = [ ];
+
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
+
+                       _luci2.rpc.batch();
+
+                       for (var i = 0; i < packages.length; i++)
+                               if (!seen[packages[i]])
+                               {
+                                       pkgs.push(packages[i]);
+                                       seen[packages[i]] = true;
+                                       self._load(packages[i]);
+                               }
+
+                       return _luci2.rpc.flush().then(function(responses) {
+                               for (var i = 0; i < responses.length; i++)
+                                       self.state.values[pkgs[i]] = responses[i];
+
+                               return pkgs;
+                       });
+               },
+
+               unload: function(packages)
+               {
+                       if (!$.isArray(packages))
+                               packages = [ packages ];
+
+                       for (var i = 0; i < packages.length; i++)
+                       {
+                               delete this.state.values[packages[i]];
+                               delete this.state.creates[packages[i]];
+                               delete this.state.changes[packages[i]];
+                               delete this.state.deletes[packages[i]];
+                       }
+               },
+
+               add: function(conf, type, name)
+               {
+                       var c = this.state.creates;
+                       var s = '.new.%d'.format(this.state.newid++);
+
+                       if (!c[conf])
+                               c[conf] = { };
+
+                       c[conf][s] = {
+                               '.type':      type,
+                               '.name':      s,
+                               '.create':    name,
+                               '.anonymous': !name,
+                               '.index':     1000 + this.state.newid
+                       };
+
+                       return s;
+               },
+
+               remove: function(conf, sid)
+               {
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
+
+                       /* requested deletion of a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf])
+                                       delete n[conf][sid];
+                       }
+                       else
+                       {
+                               if (c[conf])
+                                       delete c[conf][sid];
+
+                               if (!d[conf])
+                                       d[conf] = { };
+
+                               d[conf][sid] = true;
+                       }
+               },
+
+               sections: function(conf, type, cb)
+               {
+                       var sa = [ ];
+                       var v = this.state.values[conf];
+                       var n = this.state.creates[conf];
+                       var c = this.state.changes[conf];
+                       var d = this.state.deletes[conf];
+
+                       if (!v)
+                               return sa;
+
+                       for (var s in v)
+                               if (!d || d[s] !== true)
+                                       if (!type || v[s]['.type'] == type)
+                                               sa.push($.extend({ }, v[s], c ? c[s] : undefined));
+
+                       if (n)
+                               for (var s in n)
+                                       if (!type || n[s]['.type'] == type)
+                                               sa.push(n[s]);
+
+                       sa.sort(function(a, b) {
+                               return a['.index'] - b['.index'];
+                       });
+
+                       for (var i = 0; i < sa.length; i++)
+                               sa[i]['.index'] = i;
+
+                       if (typeof(cb) == 'function')
+                               for (var i = 0; i < sa.length; i++)
+                                       cb.call(this, sa[i], sa[i]['.name']);
+
+                       return sa;
+               },
+
+               get: function(conf, sid, opt)
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
+
+                       if (typeof(sid) == 'undefined')
+                               return undefined;
+
+                       /* requested option in a just created section */
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (!n[conf])
+                                       return undefined;
+
+                               if (typeof(opt) == 'undefined')
+                                       return n[conf][sid];
+
+                               return n[conf][sid][opt];
+                       }
+
+                       /* requested an option value */
+                       if (typeof(opt) != 'undefined')
+                       {
+                               /* check whether option was deleted */
+                               if (d[conf] && d[conf][sid])
+                               {
+                                       if (d[conf][sid] === true)
+                                               return undefined;
+
+                                       for (var i = 0; i < d[conf][sid].length; i++)
+                                               if (d[conf][sid][i] == opt)
+                                                       return undefined;
+                               }
+
+                               /* check whether option was changed */
+                               if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
+                                       return c[conf][sid][opt];
+
+                               /* return base value */
+                               if (v[conf] && v[conf][sid])
+                                       return v[conf][sid][opt];
+
+                               return undefined;
+                       }
+
+                       /* requested an entire section */
+                       if (v[conf])
+                               return v[conf][sid];
+
+                       return undefined;
+               },
+
+               set: function(conf, sid, opt, val)
+               {
+                       var n = this.state.creates;
+                       var c = this.state.changes;
+                       var d = this.state.deletes;
+
+                       if (typeof(sid) == 'undefined' ||
+                           typeof(opt) == 'undefined' ||
+                           opt.charAt(0) == '.')
+                               return;
+
+                       if (sid.indexOf('.new.') == 0)
+                       {
+                               if (n[conf] && n[conf][sid])
+                               {
+                                       if (typeof(val) != 'undefined')
+                                               n[conf][sid][opt] = val;
+                                       else
+                                               delete n[conf][sid][opt];
+                               }
+                       }
+                       else if (typeof(val) != 'undefined')
+                       {
+                               /* do not set within deleted section */
+                               if (d[conf] && d[conf][sid] === true)
+                                       return;
+
+                               if (!c[conf])
+                                       c[conf] = { };
+
+                               if (!c[conf][sid])
+                                       c[conf][sid] = { };
+
+                               /* undelete option */
+                               if (d[conf] && d[conf][sid])
+                                       d[conf][sid] = _luci2.filterArray(d[conf][sid], opt);
+
+                               c[conf][sid][opt] = val;
+                       }
+                       else
+                       {
+                               if (!d[conf])
+                                       d[conf] = { };
+
+                               if (!d[conf][sid])
+                                       d[conf][sid] = [ ];
+
+                               if (d[conf][sid] !== true)
+                                       d[conf][sid].push(opt);
+                       }
+               },
+
+               unset: function(conf, sid, opt)
+               {
+                       return this.set(conf, sid, opt, undefined);
+               },
+
+               _reload: function()
+               {
+                       var pkgs = [ ];
+
+                       for (var pkg in this.state.values)
+                               pkgs.push(pkg);
+
+                       this.init();
+
+                       return this.load(pkgs);
+               },
+
+               _reorder: function()
+               {
+                       var v = this.state.values;
+                       var n = this.state.creates;
+                       var r = this.state.reorder;
+
+                       if ($.isEmptyObject(r))
+                               return _luci2.deferrable();
+
+                       _luci2.rpc.batch();
+
+                       /*
+                        gather all created and existing sections, sort them according
+                        to their index value and issue an uci order call
+                       */
+                       for (var c in r)
+                       {
+                               var o = [ ];
+
+                               if (n && n[c])
+                                       for (var s in n[c])
+                                               o.push(n[c][s]);
+
+                               for (var s in v[c])
+                                       o.push(v[c][s]);
+
+                               if (o.length > 0)
+                               {
+                                       o.sort(function(a, b) {
+                                               return (a['.index'] - b['.index']);
+                                       });
+
+                                       var sids = [ ];
+
+                                       for (var i = 0; i < o.length; i++)
+                                               sids.push(o[i]['.name']);
+
+                                       this._order(c, sids);
+                               }
+                       }
+
+                       this.state.reorder = { };
+                       return _luci2.rpc.flush();
+               },
+
+               swap: function(conf, sid1, sid2)
+               {
+                       var s1 = this.get(conf, sid1);
+                       var s2 = this.get(conf, sid2);
+                       var n1 = s1 ? s1['.index'] : NaN;
+                       var n2 = s2 ? s2['.index'] : NaN;
+
+                       if (isNaN(n1) || isNaN(n2))
+                               return false;
+
+                       s1['.index'] = n2;
+                       s2['.index'] = n1;
+
+                       this.state.reorder[conf] = true;
+
+                       return true;
+               },
+
+               save: function()
+               {
+                       _luci2.rpc.batch();
+
+                       var self = this;
+                       var snew = [ ];
+
+                       if (self.state.creates)
+                               for (var c in self.state.creates)
+                                       for (var s in self.state.creates[c])
+                                       {
+                                               var r = {
+                                                       config: c,
+                                                       values: { }
+                                               };
+
+                                               for (var k in self.state.creates[c][s])
+                                               {
+                                                       if (k == '.type')
+                                                               r.type = self.state.creates[c][s][k];
+                                                       else if (k == '.create')
+                                                               r.name = self.state.creates[c][s][k];
+                                                       else if (k.charAt(0) != '.')
+                                                               r.values[k] = self.state.creates[c][s][k];
+                                               }
+
+                                               snew.push(self.state.creates[c][s]);
+
+                                               self._add(r.config, r.type, r.name, r.values);
+                                       }
+
+                       if (self.state.changes)
+                               for (var c in self.state.changes)
+                                       for (var s in self.state.changes[c])
+                                               self._set(c, s, self.state.changes[c][s]);
+
+                       if (self.state.deletes)
+                               for (var c in self.state.deletes)
+                                       for (var s in self.state.deletes[c])
+                                       {
+                                               var o = self.state.deletes[c][s];
+                                               self._delete(c, s, (o === true) ? undefined : o);
+                                       }
+
+                       return _luci2.rpc.flush().then(function(responses) {
+                               /*
+                                array "snew" holds references to the created uci sections,
+                                use it to assign the returned names of the new sections
+                               */
+                               for (var i = 0; i < snew.length; i++)
+                                       snew[i]['.name'] = responses[i];
+
+                               return self._reorder();
+                       });
+               },
+
+               _apply: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'apply',
+                       params: [ 'timeout', 'rollback' ]
+               }),
+
+               _confirm: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'confirm'
+               }),
+
+               apply: function(timeout)
+               {
+                       var self = this;
+                       var date = new Date();
+                       var deferred = $.Deferred();
+
+                       if (typeof(timeout) != 'number' || timeout < 1)
+                               timeout = 10;
+
+                       self._apply(timeout, true).then(function(rv) {
+                               if (rv != 0)
+                               {
+                                       deferred.rejectWith(self, [ rv ]);
+                                       return;
+                               }
+
+                               var try_deadline = date.getTime() + 1000 * timeout;
+                               var try_confirm = function()
+                               {
+                                       return self._confirm().then(function(rv) {
+                                               if (rv != 0)
+                                               {
+                                                       if (date.getTime() < try_deadline)
+                                                               window.setTimeout(try_confirm, 250);
+                                                       else
+                                                               deferred.rejectWith(self, [ rv ]);
+
+                                                       return;
+                                               }
+
+                                               deferred.resolveWith(self, [ rv ]);
+                                       });
+                               };
+
+                               window.setTimeout(try_confirm, 1000);
+                       });
+
+                       return deferred;
+               },
+
+               changes: _luci2.rpc.declare({
+                       object: 'uci',
+                       method: 'changes',
+                       expect: { changes: { } }
+               }),
+
+               readable: function(conf)
+               {
+                       return _luci2.session.hasACL('uci', conf, 'read');
+               },
+
+               writable: function(conf)
+               {
+                       return _luci2.session.hasACL('uci', conf, 'write');
+               }
+       });
+
        this.uci = {
 
                writable: function()
@@ -1626,6 +2205,41 @@ function LuCI2()
                                window.clearInterval(this._hearbeatInterval);
                                delete this._hearbeatInterval;
                        }
+               },
+
+
+               _acls: { },
+
+               _fetch_acls: _luci2.rpc.declare({
+                       object: 'session',
+                       method: 'access',
+                       expect: { '': { } }
+               }),
+
+               _fetch_acls_cb: function(acls)
+               {
+                       _luci2.session._acls = acls;
+               },
+
+               updateACLs: function()
+               {
+                       return _luci2.session._fetch_acls()
+                               .then(_luci2.session._fetch_acls_cb);
+               },
+
+               hasACL: function(scope, object, func)
+               {
+                       var acls = _luci2.session._acls;
+
+                       if (typeof(func) == 'undefined')
+                               return (acls && acls[scope] && acls[scope][object]);
+
+                       if (acls && acls[scope] && acls[scope][object])
+                               for (var i = 0; i < acls[scope][object].length; i++)
+                                       if (acls[scope][object][i] == func)
+                                               return true;
+
+                       return false;
                }
        };
 
@@ -2920,7 +3534,7 @@ function LuCI2()
                                                                else if (typeof types[label] == 'function')
                                                                {
                                                                        stack.push(types[label]);
-                                                                       stack.push(null);
+                                                                       stack.push([ ]);
                                                                }
                                                                else
                                                                {
@@ -2939,7 +3553,7 @@ function LuCI2()
                                                                throw "Syntax error, argument list follows non-function";
 
                                                        stack[stack.length-1] =
-                                                               arguments.callee(code.substring(pos, i));
+                                                               _luci2.cbi.validation.compile(code.substring(pos, i));
 
                                                        pos = i+1;
                                                }
@@ -3389,6 +4003,7 @@ function LuCI2()
                        }
 
                        i.error = $('<div />')
+                               .hide()
                                .addClass('label label-danger');
 
                        i.widget = $('<div />')
@@ -3525,7 +4140,7 @@ function LuCI2()
                                        d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
                                        d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
 
-                                       d.inst.error.text(_luci2.tr('Field must not be empty'));
+                                       d.inst.error.text(_luci2.tr('Field must not be empty')).show();
                                        rv = false;
                                }
                                else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
@@ -3533,7 +4148,7 @@ function LuCI2()
                                        d.elem.parents('div.form-group, td').first().addClass('luci2-form-error');
                                        d.elem.parents('div.input-group, div.form-group, td').first().addClass('has-error');
 
-                                       d.inst.error.text(validation.message.format.apply(validation.message, vstack[1]));
+                                       d.inst.error.text(validation.message.format.apply(validation.message, vstack[1])).show();
                                        rv = false;
                                }
                                else
@@ -3544,7 +4159,7 @@ function LuCI2()
                                        if (d.multi && d.inst.widget && d.inst.widget.find('input.error, select.error').length > 0)
                                                rv = false;
                                        else
-                                               d.inst.error.text('');
+                                               d.inst.error.text('').hide();
                                }
                        }
 
@@ -3749,7 +4364,6 @@ function LuCI2()
                        if (typeof(o.disabled) == 'undefined') o.disabled = '0';
 
                        var i = $('<input />')
-                               .addClass('form-control')
                                .attr('id', this.id(sid))
                                .attr('type', 'checkbox')
                                .prop('checked', this.ucivalue(sid));
@@ -3902,16 +4516,13 @@ function LuCI2()
                                for (var i = 0; i < this.choices.length; i++)
                                {
                                        $('<label />')
+                                               .addClass('checkbox')
                                                .append($('<input />')
-                                                       .addClass('cbi-input-checkbox')
                                                        .attr('type', 'checkbox')
                                                        .attr('value', this.choices[i][0])
                                                        .prop('checked', s[this.choices[i][0]]))
                                                .append(this.choices[i][1])
                                                .appendTo(t);
-
-                                       $('<br />')
-                                               .appendTo(t);
                                }
 
                        return t;
@@ -4427,13 +5038,12 @@ function LuCI2()
 
                                        $('<li />')
                                                .append($('<label />')
-                                                       .addClass('radio inline')
+                                                       .addClass(itype + ' inline')
                                                        .append($('<input />')
                                                                .attr('name', itype + id)
                                                                .attr('type', itype)
                                                                .attr('value', iface['interface'])
-                                                               .prop('checked', !!check[iface['interface']])
-                                                               .addClass('form-control'))
+                                                               .prop('checked', !!check[iface['interface']]))
                                                        .append(badge))
                                                .appendTo(ul);
                                }
@@ -4443,13 +5053,12 @@ function LuCI2()
                        {
                                $('<li />')
                                        .append($('<label />')
-                                               .addClass('radio inline text-muted')
+                                               .addClass(itype + ' inline text-muted')
                                                .append($('<input />')
                                                        .attr('name', itype + id)
                                                        .attr('type', itype)
                                                        .attr('value', '')
-                                                       .prop('checked', !value)
-                                                       .addClass('form-control'))
+                                                       .prop('checked', !value))
                                                .append(_luci2.tr('unspecified')))
                                        .appendTo(ul);
                        }
@@ -4614,19 +5223,21 @@ function LuCI2()
                                                inval++;
 
                                if (inval > 0)
-                                       stbadge.text(inval)
+                                       stbadge.show()
+                                               .text(inval)
                                                .attr('title', _luci2.trp('1 Error', '%d Errors', inval).format(inval));
                                else
-                                       stbadge.text('');
+                                       stbadge.hide();
 
                                invals += inval;
                        }
 
                        if (invals > 0)
-                               badge.text(invals)
+                               badge.show()
+                                       .text(invals)
                                        .attr('title', _luci2.trp('1 Error', '%d Errors', invals).format(invals));
                        else
-                               badge.text('');
+                               badge.hide();
 
                        return invals;
                },
@@ -4648,10 +5259,11 @@ function LuCI2()
                        var badge = $('#' + this.id('sectiontab')).children('span:first');
 
                        if (this.error_count > 0)
-                               badge.text(this.error_count)
+                               badge.show()
+                                       .text(this.error_count)
                                        .attr('title', _luci2.trp('1 Error', '%d Errors', this.error_count).format(this.error_count));
                        else
-                               badge.text('');
+                               badge.hide();
 
                        return (this.error_count == 0);
                }