+
+/*
+ *
+ * Copyright 2016 RIFT.IO Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// rw.js will no longer be necessary when Angular dependency no longer exists
+/**
+ * reset values in an array, useful when an array instance is
+ * being observed for changes and simply setting array reference
+ * to a new array instance is not a great option.
+ *
+ * Example:
+ * x = ['a', 'b']
+ * setValues(x, ['c', 'd'])
+ * x
+ * ['c', 'd']
+ */
+
+var rw = rw || {
+ // Angular specific for now but can be modified in one location if need ever be
+ BaseController: function() {
+ var self = this;
+
+ // handles factories with detach/cancel methods and listeners with cancel method
+ if (this.$scope) {
+ this.$scope.$on('$stateChangeStart', function() {
+ var properties = Object.getOwnPropertyNames(self);
+
+ properties.forEach(function(key) {
+
+ var propertyValue = self[key];
+
+ if (propertyValue) {
+ if (Array.isArray(propertyValue)) {
+ propertyValue.forEach(function(item) {
+ if (item.off && typeof item.off == 'function') {
+ item.off(null, null, self);
+ }
+ });
+ propertyValue.length = 0;
+ } else {
+ if (propertyValue.detached && typeof propertyValue.detached == 'function') {
+ propertyValue.detached();
+ } else if (propertyValue.cancel && typeof propertyValue.cancel == 'function') {
+ propertyValue.cancel();
+ } else if (propertyValue.off && typeof propertyValue.off == 'function') {
+ propertyValue.off(null, null, self);
+ }
+ }
+ }
+ });
+ });
+ };
+
+ // call in to do additional cleanup
+ if (self.doCleanup && typeof self.doCleanup == 'function') {
+ self.doCleanup();
+ };
+ },
+ getSearchParams: function (url) {
+ var a = document.createElement('a');
+ a.href = url;
+ var params = {};
+ var items = a.search.replace('?', '').split('&');
+ for (var i = 0; i < items.length; i++) {
+ if (items[i].length > 0) {
+ var key_value = items[i].split('=');
+ params[key_value[0]] = key_value[1];
+ }
+ }
+ return params;
+ },
+
+ inplaceUpdate : function(ary, values) {
+ var args = [0, ary.length];
+ Array.prototype.splice.apply(ary, args.concat(values));
+ }
+};
+
+// explore making this configurable somehow
+// api_server = 'http://localhost:5050';
+rw.search_params = rw.getSearchParams(window.location.href);
+// MONKEY PATCHING
+if (Element.prototype == null) {
+ Element.prototype.uniqueId = 0;
+}
+
+Element.prototype.generateUniqueId = function() {
+ Element.prototype.uniqueId++;
+ return 'uid' + Element.prototype.uniqueId;
+};
+
+Element.prototype.empty = Element.prototype.empty || function() {
+ while(this.firstChild) {
+ this.removeChild(this.firstChild);
+ }
+};
+
+/**
+ * Merge one object into another. No circular reference checking so if there
+ * is there might be infinite recursion.
+ */
+rw.merge = function(obj1, obj2) {
+ for (prop in obj2) {
+ if (typeof(obj2[prop]) == 'object') {
+ if (prop in obj1) {
+ this.merge(obj1[prop], obj2[prop]);
+ } else {
+ obj1[prop] = obj2[prop];
+ }
+ } else {
+ obj1[prop] = obj2[prop];
+ }
+ }
+}
+
+Element.prototype.getElementByTagName = function(tagName) {
+ for (var i = this.children.length - 1; i >= 0; i--) {
+ if (this.children[i].localName == tagName) {
+ return this.children[i];
+ }
+ }
+};
+
+rw.ui = {
+
+ computedWidth : function(elem, defaultValue) {
+ var s = window.getComputedStyle(elem);
+ var w = s['width'];
+ if (w && w != 'auto') {
+ // I've never seen this case, but here anyway
+ return w;
+ }
+ w = s['min-width'];
+ if (w) {
+ return w;
+ }
+ return defaultValue;
+ },
+
+ computedHeight : function(elem, defaultValue) {
+ var s = window.getComputedStyle(elem);
+ var w = s['height'];
+ if (w && w != 'auto') {
+ // I've never seen this case, but here anyway
+ return w;
+ }
+ w = s['min-height'];
+ if (w) {
+ return w;
+ }
+ return defaultValue;
+ },
+
+ computedStyle : function(elem, property, defaultValue) {
+ var s = window.getComputedStyle(elem);
+ if (s[property]) {
+ return s[property];
+ }
+ return defaultValue;
+ },
+
+ odd : function(n) {
+ return Math.abs(n) % 2 == 1 ? 'odd' : '';
+ },
+
+ status : function(s) {
+ return s == 'OK' ? 'yes' : 'no';
+ },
+
+ capitalize: function(s) {
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
+ },
+
+ fmt: function(n, fmtStr) {
+ return numeral(n).format(fmtStr);
+ },
+
+ // assumes values are in megabytes!
+ bytes: function(n, capacity) {
+ if (n === undefined || isNaN(n)) {
+ return '';
+ }
+ var units = false;
+ if (capacity === undefined) {
+ capacity = n;
+ units = true;
+ }
+ var suffixes = [
+ ['KB' , 1000],
+ ['MB' , 1000000],
+ ['GB' , 1000000000],
+ ['TB' , 1000000000000],
+ ['PB' , 1000000000000000]
+ ];
+ for (var i = 0; i < suffixes.length; i++) {
+ if (capacity < suffixes[i][1]) {
+ return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
+ }
+ }
+ return n + (units ? 'B' : '');
+ },
+
+ // assumes values are already in megabits!
+ bits: function(n, capacity) {
+ if (n === undefined || isNaN(n)) {
+ return '';
+ }
+ var units = false;
+ if (capacity === undefined) {
+ capacity = n;
+ units = true;
+ }
+ var suffixes = [
+ ['Mbps' , 1000],
+ ['Gbps' , 1000000],
+ ['Tbps' , 1000000000],
+ ['Pbps' , 1000000000000]
+ ];
+ for (var i = 0; i < suffixes.length; i++) {
+ if (capacity < suffixes[i][1]) {
+ return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
+ }
+ }
+ return n + (units ? 'Bps' : '');
+ },
+
+ ppsUtilization: function(pps) {
+ return pps ? numeral(pps / 1000000).format('0.0') : '';
+ },
+ ppsUtilizationMax: function(item) {
+ var rate = item.rate / 10000;
+ var max = item.max * 0.0015;
+ return rate/max;
+ },
+ bpsAsPps: function(speed) {
+ return parseInt(speed) * 0.0015;
+ },
+
+ upperCase: function(s) {
+ return s.toUpperCase()
+ },
+
+ mbpsAsPps: function(mbps) {
+ var n = parseInt(mbps);
+ return isNaN(n) ? 0 : rw.ui.fmt(rw.ui.bpsAsPps(n * 1000000), '0a').toUpperCase();
+ },
+
+ k: function(n) {
+ return rw.ui.fmt(rw.ui.noNaN(n), '0a');
+ },
+
+ noNaN: function(n) {
+ return isNaN(n) ? 0 : n;
+ },
+
+ // Labels used in system
+ l10n : {
+ vnf: {
+ 'trafsimclient': 'Traf Sim Client',
+ 'trafsimserver': 'Traf Sim Server',
+ 'ltemmesim': 'MME',
+ 'ltegwsim': 'SAE Gateway',
+ 'trafgen': 'Traf Gen Client',
+ 'trafsink': 'Traf Gen Server',
+ 'loadbal': 'Load Balancer',
+ 'slbalancer': 'Scriptable Load Balancer'
+ }
+ }
+};
+
+rw.math = {
+ editXml : function(xmlTemplate, domEditor) {
+ var str2dom = new DOMParser();
+ var dom = str2dom.parseFromString(xmlTemplate, 'text/xml');
+ if (domEditor) {
+ domEditor(dom);
+ }
+ var dom2str = new XMLSerializer();
+ return dom2str.serializeToString(dom);
+ },
+
+ num : function(el, tag) {
+ return parseInt(this.str(el, tag), 10);
+ },
+
+ str : function(el, tag) {
+ var tags = el.getElementsByTagName(tag);
+ return tags.length > 0 ? tags[0].textContent.trim() : '';
+ },
+
+ sum : function(total, i, key, value) {
+ if (_.isNumber(value)) {
+ total[key] = (i === 0 ? value : (total[key] + value));
+ }
+ },
+
+ sum2 : function(key) {
+ return function(prev, cur, i) {
+ var value = cur[key];
+ if (_.isNumber(value)) {
+ if (typeof(prev) === 'undefined') {
+ return value;
+ } else {
+ return prev + value;
+ }
+ }
+ return prev;
+ };
+ },
+
+ max : function(key) {
+ return function(prev, cur, i) {
+ var value = cur[key];
+ if (_.isNumber(value)) {
+ if (typeof(prev) === 'undefined') {
+ return value;
+ } else if (prev < value) {
+ return value;
+ }
+ }
+ return prev;
+ };
+ },
+
+ avg2 : function(key) {
+ var sum = rw.math.sum2(key);
+ return function(prev, cur, i, ary) {
+ var s = sum(prev, cur, i);
+ if (i === ary.length - 1) {
+ return s / ary.length;
+ }
+ return s;
+ };
+ },
+
+ avg : function(rows, key) {
+ var total = XmlMath.total(rows, key);
+ return total / rows.length;
+ },
+
+ total : function(rows, key) {
+ var total = 0;
+ for (var i = rows.length - 1; i >= 0; i--) {
+ var n = parseInt(rows[i][key]);
+ if (!isNaN(n)) {
+ total += n;
+ }
+ }
+ return total;
+ },
+
+ run : function(total, rows, operation) {
+ var i;
+ var f = function(value, key) {
+ operation(total, i, key, value);
+ };
+ for (i = 0; i < rows.length; i++) {
+ _.each(rows[i], f);
+ }
+ }
+};
+
+
+rw.db = {
+ open: function (name, onInit, onOpen) {
+ var self = this;
+
+ var open = window.indexedDB.open(name, 2);
+
+ open.onerror = function (e) {
+ console.log('Could not open database', name, e.target.error.message);
+ };
+
+ open.onsuccess = function (e) {
+ var db = e.target.result;
+ onOpen(db);
+ };
+
+ open.onupgradeneeded = function (e) {
+ var db = e.target.result;
+ onInit(db);
+ };
+ }
+};
+
+rw.db.Offline = function(name) {
+ this.name = name;
+ this.datastore = 'offline';
+};
+
+rw.db.Offline.prototype = {
+
+ open : function(onOpen) {
+ rw.db.open(this.name, this.init.bind(this), onOpen);
+ },
+
+ getItem : function(url, onData) {
+ var self = this;
+ this.open(function(db) {
+ var query = db.transaction(self.datastore)
+ .objectStore(self.datastore)
+ .get(url);
+ query.onsuccess = function(e) {
+ if (e.target.result) {
+ onData(e.target.result.data);
+ } else {
+ console.log('No data found for ' + url + '. You may need to rebuild your offline database');
+ }
+ }
+ });
+ },
+
+ init : function(db) {
+ var self = this;
+ if (!db.objectStoreNames.contains(this.datastore)) {
+ var create = db.createObjectStore(this.datastore, {keyPath: 'url'});
+ create.onerror = function(e) {
+ console.log('Could not create object store ' + this.datastore);
+ }
+ }
+ },
+
+ saveStore : function(store) {
+ var self = this;
+ this.open(function(db) {
+ for (var i = 0; i < store.length; i++) {
+ var save = db.transaction(self.datastore, "readwrite")
+ .objectStore(self.datastore)
+ .put(store[i]);
+ }
+ });
+ }
+};
+
+rw.api = {
+ nRequests : 0,
+ tRequestsMs: 0,
+ tRequestMaxMs: 0,
+ nRequestsByUrl: {},
+ statsId: null,
+
+ resetStats: function() {
+ rw.api.nRequests = 0;
+ rw.api.tRequestsMs = 0;
+ rw.api.tRequestMaxMs = 0;
+ rw.api.nRequestsByUrl = {};
+ },
+
+ handleAjaxError : function (req, status, err) {
+ console.log('failed', req, status, err);
+ },
+
+ json: function(url) {
+ return this.get(url, 'application/json')
+ },
+
+ get: function(url, accept) {
+ var deferred = jQuery.Deferred();
+ if (rw.api.offline) {
+ rw.api.offline.getItem(url, function (data) {
+ deferred.resolve(data);
+ });
+ } else {
+ var startTime = new Date().getTime();
+ jQuery.ajax(rw.api.server + url, {
+ type: 'GET',
+ dataType: 'json',
+ error: rw.api.handleAjaxError,
+ headers: rw.api.headers(accept),
+ success: function (data) {
+ rw.api.recordUrl(url, startTime);
+ deferred.resolve(data);
+ },
+ error: function(e) {
+ deferred.reject(e);
+ }
+ });
+ }
+ return deferred.promise();
+ },
+
+ headers: function(accept, contentType) {
+ var h = {
+ Accept: accept
+ };
+ if (rw.api.statsId != null) {
+ h['x-stats'] = rw.api.statsId;
+ }
+ if (contentType) {
+ h['Content-Type'] = contentType;
+ }
+ return h;
+ },
+
+ recordUrl:function(url, startTime) {
+ var elapsed = new Date().getTime() - startTime;
+ rw.api.tRequestsMs += elapsed;
+ rw.api.nRequests += 1;
+ rw.api.tRequestMaxMs = Math.max(rw.api.tRequestMaxMs, elapsed);
+ if (url in rw.api.nRequestsByUrl) {
+ var metric = rw.api.nRequestsByUrl[url];
+ metric.url = url;
+ metric.n += 1;
+ metric.max = Math.max(metric.max, elapsed);
+ } else {
+ rw.api.nRequestsByUrl[url] = {url: url, n: 1, max: elapsed};
+ }
+ },
+
+ put: function(url, data, contentType) {
+ return this.push('PUT', url, data, contentType);
+ },
+
+ post: function(url, data, contentType) {
+ return this.push('POST', url, data, contentType);
+ },
+
+ rpc: function(url, data, error) {
+ if(error === undefined){
+ error = function(a,b,c){
+ }
+ }
+ return this.push('POST', url, data, 'application/vnd.yang.data+json', error);
+ },
+
+ push: function(method, url, data, contentType, errorFn) {
+ var deferred = jQuery.Deferred();
+ if (rw.api.offline) {
+ // eating offline put request
+ if(contentType == 'application/vnd.yang.data+json'){
+ var payload = data;
+ rw.api.offline.getItem(url, function (data) {
+ deferred.resolve(data);
+ });
+ }
+ deferred.resolve({});
+ } else {
+ var startTime = new Date().getTime();
+ jQuery.ajax(rw.api.server + url, {
+ type: method,
+ error: rw.api.handleAjaxError,
+ dataType: 'json',
+ headers: rw.api.headers('application/json', contentType),
+ data: JSON.stringify(data),
+ success: function (response) {
+ rw.api.recordUrl(url, startTime);
+ deferred.resolve(response);
+ },
+ error: errorFn
+ });
+ }
+ return deferred.promise();
+ },
+
+ setOffline: function(name) {
+ if (name) {
+ rw.api.offline = new rw.db.Offline(name);
+ } else {
+ rw.api.offline = false;
+ }
+ },
+
+ // When passing things to ConfD ('/api/...') then '/' needs to be
+ // %252F(browser) --> %2F(Rest) --> / ConfD
+ encodeUrlParam: function(value) {
+ var once = rw.api.singleEncodeUrlParam(value);
+ return once.replace(/%/g, '%25');
+ },
+
+ // UrlParam cannot have '/' and encoding it using %2F gets double-encoded in flask
+ singleEncodeUrlParam: function(value) {
+ return value.replace(/\//g, '%2F');
+ }
+};
+
+rw.api.SocketSubscriber = function(url) {
+ this.url = url;
+
+ this.id = ++rw.api.SocketSubscriber.uniqueId;
+
+ this.subscribed = false;
+ this.offlineRateMs = 2000;
+},
+
+rw.api.SocketSubscriber.uniqueId = 0;
+
+rw.api.SocketSubscriber.prototype = {
+
+ // does not support PUT/PORT with payloads requests yet
+ websubscribe : function(webUrl, onload, offline) {
+ this.subscribeMeta(onload, {
+ url: webUrl
+ }, offline);
+ },
+
+ subscribeMeta : function(onload, meta, offline) {
+ var self = this;
+
+ if (this.subscribed) {
+ this.unsubscribe();
+ }
+
+ var m = meta || {};
+ if (rw.api.offline) {
+ this.subscribeOffline(onload, m, offline);
+ } else {
+ this.subscribeOnline(onload, m);
+ }
+ },
+
+ subscribeOffline: function(onload, meta, offline) {
+ var self = this;
+ rw.api.json(meta.url).done(function(data) {
+ var _update = function() {
+ if (offline) {
+ offline(data);
+ } else {
+ onload(data);
+ }
+ };
+
+ this.offlineTimer = setInterval(_update, self.offlineRateMs);
+ });
+ },
+
+ subscribeOnline: function(onload, meta) {
+ var self = this;
+ var _subscribe = function() {
+ meta.widgetId = self.id;
+ meta.accept = meta.accept || 'application/json';
+ document.websocket().emit(self.url, meta);
+ self.subscribed = true;
+ };
+
+ var _register = function() {
+ document.websocket().on(self.url + '/' + self.id, function(dataString) {
+ var data = dataString;
+
+ // auto convert to object to make backward compatible
+ if (meta.accept.match(/[+\/]json$/) && dataString != '') {
+ data = JSON.parse(dataString);
+ }
+ onload(data);
+ });
+ _subscribe();
+ };
+ document.websocket().on('error',function(d){
+ console.log('socket error', d)
+ });
+ document.websocket().on('close',function(d){
+ console.log('socket close', d)
+ })
+ document.websocket().on('connect', _register);
+
+ // it's possible this call is not nec. and will always be called
+ // as part of connect statement above
+ _register();
+ },
+
+ unsubscribe : function() {
+ if (rw.api.offline) {
+ if (this.offlineTimer) {
+ clearInterval(this.offlineTimer);
+ this.offlineTimer = null;
+ }
+ } else {
+ var unsubscribe = { widgetId: this.id, enable: false };
+ document.websocket().emit(this.url, unsubscribe);
+ this.subscribed = false;
+ }
+ }
+};
+
+rw.api.server = rw.search_params['api_server'] || '';
+
+document.websocket = function() {
+ if ( ! this.socket ) {
+ //io.reconnection = true;
+ var wsServer = rw.api.server || 'http://' + document.domain + ':' + location.port;
+ var wsUrl = wsServer + '/rwapp';
+ this.socket = io.connect(wsUrl, {reconnection:true});
+ }
+ return this.socket;
+};
+
+rw.api.setOffline(rw.search_params['offline']);
+
+rw.vnf = {
+ ports: function(service) {
+ return _.flatten(jsonPath.eval(service, '$.connector[*].interface[*].port'));
+ },
+ fabricPorts: function(service) {
+ return _.flatten(jsonPath.eval(service, '$.vm[*].fabric.port'));
+ }
+};
+
+rw.VcsVisitor = function(enter, leave) {
+ this.enter = enter;
+ this.leave = leave;
+}
+
+rw.VcsVisitor.prototype = {
+
+ visit: function(node, parent, i, listType) {
+ var hasChildren = this.enter(parent, node, i, listType);
+ if (hasChildren) {
+ switch (rw.vcs.nodeType(node)) {
+ case 'rwsector':
+ this.visitChildren(node, node.collection, 'collection');
+ break;
+ case 'rwcolony':
+ this.visitChildren(node, node.collection, 'collection');
+ this.visitChildren(node, node.vm, 'vm');
+ break;
+ case 'rwcluster':
+ this.visitChildren(node, node.vm, 'vm');
+ break;
+ case 'RWVM':
+ this.visitChildren(node, node.process, 'process');
+ break;
+ case 'RWPROC':
+ this.visitChildren(node, node.tasklet, 'tasklet');
+ break;
+ }
+ }
+ if (this.leave) {
+ this.leave(parent, node, obj, i, listType);
+ }
+ },
+
+ visitChildren : function(parent, children, listType) {
+ if (!children) {
+ return;
+ }
+ var i = 0;
+ var self = this;
+ _.each(children, function(child) {
+ self.visit.call(self, child, parent, i, listType);
+ i += 1;
+ });
+ }
+};
+
+rw.vcs = {
+
+ allVms : function() {
+ return _.flatten([this.jpath('$.collection[*].vm'), this.jpath('$.collection[*].collection[*].vm')], true);
+ },
+
+ vms: function(n) {
+ if (n == undefined || n === null || n === this) {
+ return this.allVms();
+ }
+ switch (rw.vcs.nodeType(n)) {
+ case 'rwcolony':
+ return this.jpath('$.collection[*].vm[*]', n);
+ case 'rwcluster':
+ return this.jpath('$.vm[*]', n);
+ case 'RWVM':
+ return [n];
+ default:
+ return null;
+ }
+ },
+
+ nodeType: function(node) {
+ if (node.component_type === 'RWCOLLECTION') {
+ return node.collection_info['collection-type'];
+ }
+ return node.component_type;
+ },
+
+ allClusters : function() {
+ return this.jpath('$.collection[*].collection');
+ },
+
+ allColonies: function() {
+ return this.jpath('$.collection');
+ },
+
+ allPorts:function(n) {
+ switch (rw.vcs.nodeType(n)) {
+ case 'rwsector':
+ return this.jpath('$.collection[*].collection[*].vm[*].port[*]', n);
+ case 'rwcolony':
+ return this.jpath('$.collection[*].vm[*].port[*]', n);
+ case 'rwcluster':
+ return this.jpath('$.vm[*].port[*]', n);
+ case 'RWVM':
+ return this.jpath('$.port[*]', n);
+ default:
+ return null;
+ }
+ },
+
+ allFabricPorts:function(n) {
+ switch (rw.vcs.nodeType(n)) {
+ case 'rwsector':
+ return this.jpath('$.collection[*].collection[*].vm[*].fabric.port[*]', n);
+ case 'rwcolony':
+ return this.jpath('$.collection[*].vm[*].fabric.port[*]', n);
+ case 'rwcluster':
+ return this.jpath('$.vm[*].fabric.port[*]', n);
+ case 'RWVM':
+ return this.jpath('$.fabric.port[*]', n);
+ default:
+ return null;
+ }
+ },
+
+ getChildren: function(n) {
+ switch (rw.vcs.nodeType(n)) {
+ case 'rwcolony':
+ return 'vm' in n ? _.union(n.collection, n.vm) : n.collection;
+ case 'rwcluster':
+ return n.vm;
+ case 'RWVM':
+ return n.process;
+ case 'RWPROC':
+ return n.tasklet;
+ }
+ return [];
+ },
+
+ jpath : function(jpath, n) {
+ return _.flatten(jsonPath.eval(n || this, jpath), true);
+ }
+};
+
+rw.trafgen = {
+ startedActual : null, // true or false once server-side state is loaded
+ startedPerceived : false,
+ ratePerceived : 25,
+ rateActual : null, // non-null once server-side state is loaded
+ packetSizePerceived : 1024,
+ packetSizeActual: null
+};
+
+rw.trafsim = {
+ startedActual : null, // true or false once server-side state is loaded
+ startedPerceived : false,
+ ratePerceived : 50000,
+ rateActual : null, // non-null once server-side state is loaded
+ maxRate: 200000
+};
+
+rw.aggregateControlPanel = null;
+
+rw.theme = {
+ // purple-2 and blue-2
+ txBps : 'hsla(212, 57%, 50%, 1)',
+ txBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
+ rxBps : 'hsla(212, 57%, 50%, 1)',
+ rxBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
+ txPps : 'hsla(260, 35%, 50%, 1)',
+ txPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
+ rxPps : 'hsla(260, 35%, 50%, 1)',
+ rxPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
+ memory : 'hsla(27, 98%, 57%, 1)',
+ memoryTranslucent : 'hsla(27, 98%, 57%, 0.7)',
+ cpu : 'hsla(123, 45%, 50%, 1)',
+ cpuTranslucent : 'hsla(123, 45%, 50%, 0.7)',
+ storage : 'hsla(180, 78%, 25%, 1)',
+ storageTranslucent : 'hsla(180, 78%, 25%, 0.7)'
+};
+
+rw.RateTimer = function(onChange, onStop) {
+ this.rate = 0;
+ this.onChange = onChange;
+ this.onStop = onStop;
+ this.testFrequency = 500;
+ this.testWaveLength = 5000;
+ this.timer = null;
+ return this;
+};
+
+rw.RateTimer.prototype = {
+ start: function() {
+ this.rate = 0;
+ if (!this.timer) {
+ this.n = 0;
+ var strategy = this.smoothRateStrategy.bind(this);
+ this.testingStartTime = new Date().getTime();
+ this.timer = setInterval(strategy, this.testFrequency);
+ }
+ },
+
+ stop: function() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ this.timer = null;
+ this.onStop();
+ }
+ },
+
+ smoothRateStrategy: function() {
+ var x = (new Date().getTime() - this.testingStartTime) / this.testWaveLength;
+ this.rate = Math.round(100 * 0.5 * (1 - Math.cos(x)));
+ // in theory you could use wavelength and frequency to determine stop but this
+ // is guaranteed to stop at zero.
+ this.onChange(this.rate);
+ this.n += 1;
+ if (this.n >= 10 && this.rate < 1) {
+ this.stop();
+ }
+ }
+};
+
+module.exports = rw;