1cca8f2c4a4fba672a321970645cfd063a93d056
[osm/UI.git] / skyquake / framework / utils / rw.js
1
2 /*
3 *
4 * Copyright 2016 RIFT.IO Inc
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 *
18 */
19
20 // rw.js will no longer be necessary when Angular dependency no longer exists
21 /**
22 * reset values in an array, useful when an array instance is
23 * being observed for changes and simply setting array reference
24 * to a new array instance is not a great option.
25 *
26 * Example:
27 * x = ['a', 'b']
28 * setValues(x, ['c', 'd'])
29 * x
30 * ['c', 'd']
31 */
32
33 var rw = rw || {
34 // Angular specific for now but can be modified in one location if need ever be
35 BaseController: function() {
36 var self = this;
37
38 // handles factories with detach/cancel methods and listeners with cancel method
39 if (this.$scope) {
40 this.$scope.$on('$stateChangeStart', function() {
41 var properties = Object.getOwnPropertyNames(self);
42
43 properties.forEach(function(key) {
44
45 var propertyValue = self[key];
46
47 if (propertyValue) {
48 if (Array.isArray(propertyValue)) {
49 propertyValue.forEach(function(item) {
50 if (item.off && typeof item.off == 'function') {
51 item.off(null, null, self);
52 }
53 });
54 propertyValue.length = 0;
55 } else {
56 if (propertyValue.detached && typeof propertyValue.detached == 'function') {
57 propertyValue.detached();
58 } else if (propertyValue.cancel && typeof propertyValue.cancel == 'function') {
59 propertyValue.cancel();
60 } else if (propertyValue.off && typeof propertyValue.off == 'function') {
61 propertyValue.off(null, null, self);
62 }
63 }
64 }
65 });
66 });
67 };
68
69 // call in to do additional cleanup
70 if (self.doCleanup && typeof self.doCleanup == 'function') {
71 self.doCleanup();
72 };
73 },
74 getSearchParams: function (url) {
75 var a = document.createElement('a');
76 a.href = url;
77 var params = {};
78 var items = a.search.replace('?', '').split('&');
79 for (var i = 0; i < items.length; i++) {
80 if (items[i].length > 0) {
81 var key_value = items[i].split('=');
82 params[key_value[0]] = key_value[1];
83 }
84 }
85 return params;
86 },
87
88 inplaceUpdate : function(ary, values) {
89 var args = [0, ary.length];
90 Array.prototype.splice.apply(ary, args.concat(values));
91 }
92 };
93
94 // explore making this configurable somehow
95 // api_server = 'http://localhost:5050';
96 rw.search_params = rw.getSearchParams(window.location.href);
97 // MONKEY PATCHING
98 if (Element.prototype == null) {
99 Element.prototype.uniqueId = 0;
100 }
101
102 Element.prototype.generateUniqueId = function() {
103 Element.prototype.uniqueId++;
104 return 'uid' + Element.prototype.uniqueId;
105 };
106
107 Element.prototype.empty = Element.prototype.empty || function() {
108 while(this.firstChild) {
109 this.removeChild(this.firstChild);
110 }
111 };
112
113 /**
114 * Merge one object into another. No circular reference checking so if there
115 * is there might be infinite recursion.
116 */
117 rw.merge = function(obj1, obj2) {
118 for (prop in obj2) {
119 if (typeof(obj2[prop]) == 'object') {
120 if (prop in obj1) {
121 this.merge(obj1[prop], obj2[prop]);
122 } else {
123 obj1[prop] = obj2[prop];
124 }
125 } else {
126 obj1[prop] = obj2[prop];
127 }
128 }
129 }
130
131 Element.prototype.getElementByTagName = function(tagName) {
132 for (var i = this.children.length - 1; i >= 0; i--) {
133 if (this.children[i].localName == tagName) {
134 return this.children[i];
135 }
136 }
137 };
138
139 rw.ui = {
140
141 computedWidth : function(elem, defaultValue) {
142 var s = window.getComputedStyle(elem);
143 var w = s['width'];
144 if (w && w != 'auto') {
145 // I've never seen this case, but here anyway
146 return w;
147 }
148 w = s['min-width'];
149 if (w) {
150 return w;
151 }
152 return defaultValue;
153 },
154
155 computedHeight : function(elem, defaultValue) {
156 var s = window.getComputedStyle(elem);
157 var w = s['height'];
158 if (w && w != 'auto') {
159 // I've never seen this case, but here anyway
160 return w;
161 }
162 w = s['min-height'];
163 if (w) {
164 return w;
165 }
166 return defaultValue;
167 },
168
169 computedStyle : function(elem, property, defaultValue) {
170 var s = window.getComputedStyle(elem);
171 if (s[property]) {
172 return s[property];
173 }
174 return defaultValue;
175 },
176
177 odd : function(n) {
178 return Math.abs(n) % 2 == 1 ? 'odd' : '';
179 },
180
181 status : function(s) {
182 return s == 'OK' ? 'yes' : 'no';
183 },
184
185 capitalize: function(s) {
186 return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
187 },
188
189 fmt: function(n, fmtStr) {
190 return numeral(n).format(fmtStr);
191 },
192
193 // assumes values are in megabytes!
194 bytes: function(n, capacity) {
195 if (n === undefined || isNaN(n)) {
196 return '';
197 }
198 var units = false;
199 if (capacity === undefined) {
200 capacity = n;
201 units = true;
202 }
203 var suffixes = [
204 ['KB' , 1000],
205 ['MB' , 1000000],
206 ['GB' , 1000000000],
207 ['TB' , 1000000000000],
208 ['PB' , 1000000000000000]
209 ];
210 for (var i = 0; i < suffixes.length; i++) {
211 if (capacity < suffixes[i][1]) {
212 return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
213 }
214 }
215 return n + (units ? 'B' : '');
216 },
217
218 // assumes values are already in megabits!
219 bits: function(n, capacity) {
220 if (n === undefined || isNaN(n)) {
221 return '';
222 }
223 var units = false;
224 if (capacity === undefined) {
225 capacity = n;
226 units = true;
227 }
228 var suffixes = [
229 ['Mbps' , 1000],
230 ['Gbps' , 1000000],
231 ['Tbps' , 1000000000],
232 ['Pbps' , 1000000000000]
233 ];
234 for (var i = 0; i < suffixes.length; i++) {
235 if (capacity < suffixes[i][1]) {
236 return (numeral((n * 1000) / suffixes[i][1]).format('0,0') + (units ? suffixes[i][0] : ''));
237 }
238 }
239 return n + (units ? 'Bps' : '');
240 },
241
242 ppsUtilization: function(pps) {
243 return pps ? numeral(pps / 1000000).format('0.0') : '';
244 },
245 ppsUtilizationMax: function(item) {
246 var rate = item.rate / 10000;
247 var max = item.max * 0.0015;
248 return rate/max;
249 },
250 bpsAsPps: function(speed) {
251 return parseInt(speed) * 0.0015;
252 },
253
254 upperCase: function(s) {
255 return s.toUpperCase()
256 },
257
258 mbpsAsPps: function(mbps) {
259 var n = parseInt(mbps);
260 return isNaN(n) ? 0 : rw.ui.fmt(rw.ui.bpsAsPps(n * 1000000), '0a').toUpperCase();
261 },
262
263 k: function(n) {
264 return rw.ui.fmt(rw.ui.noNaN(n), '0a');
265 },
266
267 noNaN: function(n) {
268 return isNaN(n) ? 0 : n;
269 },
270
271 // Labels used in system
272 l10n : {
273 vnf: {
274 'trafsimclient': 'Traf Sim Client',
275 'trafsimserver': 'Traf Sim Server',
276 'ltemmesim': 'MME',
277 'ltegwsim': 'SAE Gateway',
278 'trafgen': 'Traf Gen Client',
279 'trafsink': 'Traf Gen Server',
280 'loadbal': 'Load Balancer',
281 'slbalancer': 'Scriptable Load Balancer'
282 }
283 }
284 };
285
286 rw.math = {
287 editXml : function(xmlTemplate, domEditor) {
288 var str2dom = new DOMParser();
289 var dom = str2dom.parseFromString(xmlTemplate, 'text/xml');
290 if (domEditor) {
291 domEditor(dom);
292 }
293 var dom2str = new XMLSerializer();
294 return dom2str.serializeToString(dom);
295 },
296
297 num : function(el, tag) {
298 return parseInt(this.str(el, tag), 10);
299 },
300
301 str : function(el, tag) {
302 var tags = el.getElementsByTagName(tag);
303 return tags.length > 0 ? tags[0].textContent.trim() : '';
304 },
305
306 sum : function(total, i, key, value) {
307 if (_.isNumber(value)) {
308 total[key] = (i === 0 ? value : (total[key] + value));
309 }
310 },
311
312 sum2 : function(key) {
313 return function(prev, cur, i) {
314 var value = cur[key];
315 if (_.isNumber(value)) {
316 if (typeof(prev) === 'undefined') {
317 return value;
318 } else {
319 return prev + value;
320 }
321 }
322 return prev;
323 };
324 },
325
326 max : function(key) {
327 return function(prev, cur, i) {
328 var value = cur[key];
329 if (_.isNumber(value)) {
330 if (typeof(prev) === 'undefined') {
331 return value;
332 } else if (prev < value) {
333 return value;
334 }
335 }
336 return prev;
337 };
338 },
339
340 avg2 : function(key) {
341 var sum = rw.math.sum2(key);
342 return function(prev, cur, i, ary) {
343 var s = sum(prev, cur, i);
344 if (i === ary.length - 1) {
345 return s / ary.length;
346 }
347 return s;
348 };
349 },
350
351 avg : function(rows, key) {
352 var total = XmlMath.total(rows, key);
353 return total / rows.length;
354 },
355
356 total : function(rows, key) {
357 var total = 0;
358 for (var i = rows.length - 1; i >= 0; i--) {
359 var n = parseInt(rows[i][key]);
360 if (!isNaN(n)) {
361 total += n;
362 }
363 }
364 return total;
365 },
366
367 run : function(total, rows, operation) {
368 var i;
369 var f = function(value, key) {
370 operation(total, i, key, value);
371 };
372 for (i = 0; i < rows.length; i++) {
373 _.each(rows[i], f);
374 }
375 }
376 };
377
378
379 rw.db = {
380 open: function (name, onInit, onOpen) {
381 var self = this;
382
383 var open = window.indexedDB.open(name, 2);
384
385 open.onerror = function (e) {
386 console.log('Could not open database', name, e.target.error.message);
387 };
388
389 open.onsuccess = function (e) {
390 var db = e.target.result;
391 onOpen(db);
392 };
393
394 open.onupgradeneeded = function (e) {
395 var db = e.target.result;
396 onInit(db);
397 };
398 }
399 };
400
401 rw.db.Offline = function(name) {
402 this.name = name;
403 this.datastore = 'offline';
404 };
405
406 rw.db.Offline.prototype = {
407
408 open : function(onOpen) {
409 rw.db.open(this.name, this.init.bind(this), onOpen);
410 },
411
412 getItem : function(url, onData) {
413 var self = this;
414 this.open(function(db) {
415 var query = db.transaction(self.datastore)
416 .objectStore(self.datastore)
417 .get(url);
418 query.onsuccess = function(e) {
419 if (e.target.result) {
420 onData(e.target.result.data);
421 } else {
422 console.log('No data found for ' + url + '. You may need to rebuild your offline database');
423 }
424 }
425 });
426 },
427
428 init : function(db) {
429 var self = this;
430 if (!db.objectStoreNames.contains(this.datastore)) {
431 var create = db.createObjectStore(this.datastore, {keyPath: 'url'});
432 create.onerror = function(e) {
433 console.log('Could not create object store ' + this.datastore);
434 }
435 }
436 },
437
438 saveStore : function(store) {
439 var self = this;
440 this.open(function(db) {
441 for (var i = 0; i < store.length; i++) {
442 var save = db.transaction(self.datastore, "readwrite")
443 .objectStore(self.datastore)
444 .put(store[i]);
445 }
446 });
447 }
448 };
449
450 rw.api = {
451 nRequests : 0,
452 tRequestsMs: 0,
453 tRequestMaxMs: 0,
454 nRequestsByUrl: {},
455 statsId: null,
456
457 resetStats: function() {
458 rw.api.nRequests = 0;
459 rw.api.tRequestsMs = 0;
460 rw.api.tRequestMaxMs = 0;
461 rw.api.nRequestsByUrl = {};
462 },
463
464 handleAjaxError : function (req, status, err) {
465 console.log('failed', req, status, err);
466 },
467
468 json: function(url) {
469 return this.get(url, 'application/json')
470 },
471
472 get: function(url, accept) {
473 var deferred = jQuery.Deferred();
474 if (rw.api.offline) {
475 rw.api.offline.getItem(url, function (data) {
476 deferred.resolve(data);
477 });
478 } else {
479 var startTime = new Date().getTime();
480 jQuery.ajax(rw.api.server + url, {
481 type: 'GET',
482 dataType: 'json',
483 error: rw.api.handleAjaxError,
484 headers: rw.api.headers(accept),
485 success: function (data) {
486 rw.api.recordUrl(url, startTime);
487 deferred.resolve(data);
488 },
489 error: function(e) {
490 deferred.reject(e);
491 }
492 });
493 }
494 return deferred.promise();
495 },
496
497 headers: function(accept, contentType) {
498 var h = {
499 Accept: accept
500 };
501 if (rw.api.statsId != null) {
502 h['x-stats'] = rw.api.statsId;
503 }
504 if (contentType) {
505 h['Content-Type'] = contentType;
506 }
507 return h;
508 },
509
510 recordUrl:function(url, startTime) {
511 var elapsed = new Date().getTime() - startTime;
512 rw.api.tRequestsMs += elapsed;
513 rw.api.nRequests += 1;
514 rw.api.tRequestMaxMs = Math.max(rw.api.tRequestMaxMs, elapsed);
515 if (url in rw.api.nRequestsByUrl) {
516 var metric = rw.api.nRequestsByUrl[url];
517 metric.url = url;
518 metric.n += 1;
519 metric.max = Math.max(metric.max, elapsed);
520 } else {
521 rw.api.nRequestsByUrl[url] = {url: url, n: 1, max: elapsed};
522 }
523 },
524
525 put: function(url, data, contentType) {
526 return this.push('PUT', url, data, contentType);
527 },
528
529 post: function(url, data, contentType) {
530 return this.push('POST', url, data, contentType);
531 },
532
533 rpc: function(url, data, error) {
534 if(error === undefined){
535 error = function(a,b,c){
536 }
537 }
538 return this.push('POST', url, data, 'application/vnd.yang.data+json', error);
539 },
540
541 push: function(method, url, data, contentType, errorFn) {
542 var deferred = jQuery.Deferred();
543 if (rw.api.offline) {
544 // eating offline put request
545 if(contentType == 'application/vnd.yang.data+json'){
546 var payload = data;
547 rw.api.offline.getItem(url, function (data) {
548 deferred.resolve(data);
549 });
550 }
551 deferred.resolve({});
552 } else {
553 var startTime = new Date().getTime();
554 jQuery.ajax(rw.api.server + url, {
555 type: method,
556 error: rw.api.handleAjaxError,
557 dataType: 'json',
558 headers: rw.api.headers('application/json', contentType),
559 data: JSON.stringify(data),
560 success: function (response) {
561 rw.api.recordUrl(url, startTime);
562 deferred.resolve(response);
563 },
564 error: errorFn
565 });
566 }
567 return deferred.promise();
568 },
569
570 setOffline: function(name) {
571 if (name) {
572 rw.api.offline = new rw.db.Offline(name);
573 } else {
574 rw.api.offline = false;
575 }
576 },
577
578 // When passing things to ConfD ('/api/...') then '/' needs to be
579 // %252F(browser) --> %2F(Rest) --> / ConfD
580 encodeUrlParam: function(value) {
581 var once = rw.api.singleEncodeUrlParam(value);
582 return once.replace(/%/g, '%25');
583 },
584
585 // UrlParam cannot have '/' and encoding it using %2F gets double-encoded in flask
586 singleEncodeUrlParam: function(value) {
587 return value.replace(/\//g, '%2F');
588 }
589 };
590
591 rw.api.SocketSubscriber = function(url) {
592 this.url = url;
593
594 this.id = ++rw.api.SocketSubscriber.uniqueId;
595
596 this.subscribed = false;
597 this.offlineRateMs = 2000;
598 },
599
600 rw.api.SocketSubscriber.uniqueId = 0;
601
602 rw.api.SocketSubscriber.prototype = {
603
604 // does not support PUT/PORT with payloads requests yet
605 websubscribe : function(webUrl, onload, offline) {
606 this.subscribeMeta(onload, {
607 url: webUrl
608 }, offline);
609 },
610
611 subscribeMeta : function(onload, meta, offline) {
612 var self = this;
613
614 if (this.subscribed) {
615 this.unsubscribe();
616 }
617
618 var m = meta || {};
619 if (rw.api.offline) {
620 this.subscribeOffline(onload, m, offline);
621 } else {
622 this.subscribeOnline(onload, m);
623 }
624 },
625
626 subscribeOffline: function(onload, meta, offline) {
627 var self = this;
628 rw.api.json(meta.url).done(function(data) {
629 var _update = function() {
630 if (offline) {
631 offline(data);
632 } else {
633 onload(data);
634 }
635 };
636
637 this.offlineTimer = setInterval(_update, self.offlineRateMs);
638 });
639 },
640
641 subscribeOnline: function(onload, meta) {
642 var self = this;
643 var _subscribe = function() {
644 meta.widgetId = self.id;
645 meta.accept = meta.accept || 'application/json';
646 document.websocket().emit(self.url, meta);
647 self.subscribed = true;
648 };
649
650 var _register = function() {
651 document.websocket().on(self.url + '/' + self.id, function(dataString) {
652 var data = dataString;
653
654 // auto convert to object to make backward compatible
655 if (meta.accept.match(/[+\/]json$/) && dataString != '') {
656 data = JSON.parse(dataString);
657 }
658 onload(data);
659 });
660 _subscribe();
661 };
662 document.websocket().on('error',function(d){
663 console.log('socket error', d)
664 });
665 document.websocket().on('close',function(d){
666 console.log('socket close', d)
667 })
668 document.websocket().on('connect', _register);
669
670 // it's possible this call is not nec. and will always be called
671 // as part of connect statement above
672 _register();
673 },
674
675 unsubscribe : function() {
676 if (rw.api.offline) {
677 if (this.offlineTimer) {
678 clearInterval(this.offlineTimer);
679 this.offlineTimer = null;
680 }
681 } else {
682 var unsubscribe = { widgetId: this.id, enable: false };
683 document.websocket().emit(this.url, unsubscribe);
684 this.subscribed = false;
685 }
686 }
687 };
688
689 rw.api.server = rw.search_params['api_server'] || '';
690
691 document.websocket = function() {
692 if ( ! this.socket ) {
693 //io.reconnection = true;
694 var wsServer = rw.api.server || 'http://' + document.domain + ':' + location.port;
695 var wsUrl = wsServer + '/rwapp';
696 this.socket = io.connect(wsUrl, {reconnection:true});
697 }
698 return this.socket;
699 };
700
701 rw.api.setOffline(rw.search_params['offline']);
702
703 rw.vnf = {
704 ports: function(service) {
705 return _.flatten(jsonPath.eval(service, '$.connector[*].interface[*].port'));
706 },
707 fabricPorts: function(service) {
708 return _.flatten(jsonPath.eval(service, '$.vm[*].fabric.port'));
709 }
710 };
711
712 rw.VcsVisitor = function(enter, leave) {
713 this.enter = enter;
714 this.leave = leave;
715 }
716
717 rw.VcsVisitor.prototype = {
718
719 visit: function(node, parent, i, listType) {
720 var hasChildren = this.enter(parent, node, i, listType);
721 if (hasChildren) {
722 switch (rw.vcs.nodeType(node)) {
723 case 'rwsector':
724 this.visitChildren(node, node.collection, 'collection');
725 break;
726 case 'rwcolony':
727 this.visitChildren(node, node.collection, 'collection');
728 this.visitChildren(node, node.vm, 'vm');
729 break;
730 case 'rwcluster':
731 this.visitChildren(node, node.vm, 'vm');
732 break;
733 case 'RWVM':
734 this.visitChildren(node, node.process, 'process');
735 break;
736 case 'RWPROC':
737 this.visitChildren(node, node.tasklet, 'tasklet');
738 break;
739 }
740 }
741 if (this.leave) {
742 this.leave(parent, node, obj, i, listType);
743 }
744 },
745
746 visitChildren : function(parent, children, listType) {
747 if (!children) {
748 return;
749 }
750 var i = 0;
751 var self = this;
752 _.each(children, function(child) {
753 self.visit.call(self, child, parent, i, listType);
754 i += 1;
755 });
756 }
757 };
758
759 rw.vcs = {
760
761 allVms : function() {
762 return _.flatten([this.jpath('$.collection[*].vm'), this.jpath('$.collection[*].collection[*].vm')], true);
763 },
764
765 vms: function(n) {
766 if (n == undefined || n === null || n === this) {
767 return this.allVms();
768 }
769 switch (rw.vcs.nodeType(n)) {
770 case 'rwcolony':
771 return this.jpath('$.collection[*].vm[*]', n);
772 case 'rwcluster':
773 return this.jpath('$.vm[*]', n);
774 case 'RWVM':
775 return [n];
776 default:
777 return null;
778 }
779 },
780
781 nodeType: function(node) {
782 if (node.component_type === 'RWCOLLECTION') {
783 return node.collection_info['collection-type'];
784 }
785 return node.component_type;
786 },
787
788 allClusters : function() {
789 return this.jpath('$.collection[*].collection');
790 },
791
792 allColonies: function() {
793 return this.jpath('$.collection');
794 },
795
796 allPorts:function(n) {
797 switch (rw.vcs.nodeType(n)) {
798 case 'rwsector':
799 return this.jpath('$.collection[*].collection[*].vm[*].port[*]', n);
800 case 'rwcolony':
801 return this.jpath('$.collection[*].vm[*].port[*]', n);
802 case 'rwcluster':
803 return this.jpath('$.vm[*].port[*]', n);
804 case 'RWVM':
805 return this.jpath('$.port[*]', n);
806 default:
807 return null;
808 }
809 },
810
811 allFabricPorts:function(n) {
812 switch (rw.vcs.nodeType(n)) {
813 case 'rwsector':
814 return this.jpath('$.collection[*].collection[*].vm[*].fabric.port[*]', n);
815 case 'rwcolony':
816 return this.jpath('$.collection[*].vm[*].fabric.port[*]', n);
817 case 'rwcluster':
818 return this.jpath('$.vm[*].fabric.port[*]', n);
819 case 'RWVM':
820 return this.jpath('$.fabric.port[*]', n);
821 default:
822 return null;
823 }
824 },
825
826 getChildren: function(n) {
827 switch (rw.vcs.nodeType(n)) {
828 case 'rwcolony':
829 return 'vm' in n ? _.union(n.collection, n.vm) : n.collection;
830 case 'rwcluster':
831 return n.vm;
832 case 'RWVM':
833 return n.process;
834 case 'RWPROC':
835 return n.tasklet;
836 }
837 return [];
838 },
839
840 jpath : function(jpath, n) {
841 return _.flatten(jsonPath.eval(n || this, jpath), true);
842 }
843 };
844
845 rw.trafgen = {
846 startedActual : null, // true or false once server-side state is loaded
847 startedPerceived : false,
848 ratePerceived : 25,
849 rateActual : null, // non-null once server-side state is loaded
850 packetSizePerceived : 1024,
851 packetSizeActual: null
852 };
853
854 rw.trafsim = {
855 startedActual : null, // true or false once server-side state is loaded
856 startedPerceived : false,
857 ratePerceived : 50000,
858 rateActual : null, // non-null once server-side state is loaded
859 maxRate: 200000
860 };
861
862 rw.aggregateControlPanel = null;
863
864 rw.theme = {
865 // purple-2 and blue-2
866 txBps : 'hsla(212, 57%, 50%, 1)',
867 txBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
868 rxBps : 'hsla(212, 57%, 50%, 1)',
869 rxBpsTranslucent : 'hsla(212, 57%, 50%, 0.7)',
870 txPps : 'hsla(260, 35%, 50%, 1)',
871 txPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
872 rxPps : 'hsla(260, 35%, 50%, 1)',
873 rxPpsTranslucent : 'hsla(260, 35%, 50%, 0.7)',
874 memory : 'hsla(27, 98%, 57%, 1)',
875 memoryTranslucent : 'hsla(27, 98%, 57%, 0.7)',
876 cpu : 'hsla(123, 45%, 50%, 1)',
877 cpuTranslucent : 'hsla(123, 45%, 50%, 0.7)',
878 storage : 'hsla(180, 78%, 25%, 1)',
879 storageTranslucent : 'hsla(180, 78%, 25%, 0.7)'
880 };
881
882 rw.RateTimer = function(onChange, onStop) {
883 this.rate = 0;
884 this.onChange = onChange;
885 this.onStop = onStop;
886 this.testFrequency = 500;
887 this.testWaveLength = 5000;
888 this.timer = null;
889 return this;
890 };
891
892 rw.RateTimer.prototype = {
893 start: function() {
894 this.rate = 0;
895 if (!this.timer) {
896 this.n = 0;
897 var strategy = this.smoothRateStrategy.bind(this);
898 this.testingStartTime = new Date().getTime();
899 this.timer = setInterval(strategy, this.testFrequency);
900 }
901 },
902
903 stop: function() {
904 if (this.timer) {
905 clearInterval(this.timer);
906 this.timer = null;
907 this.onStop();
908 }
909 },
910
911 smoothRateStrategy: function() {
912 var x = (new Date().getTime() - this.testingStartTime) / this.testWaveLength;
913 this.rate = Math.round(100 * 0.5 * (1 - Math.cos(x)));
914 // in theory you could use wavelength and frequency to determine stop but this
915 // is guaranteed to stop at zero.
916 this.onChange(this.rate);
917 this.n += 1;
918 if (this.n >= 10 && this.rate < 1) {
919 this.stop();
920 }
921 }
922 };
923
924 module.exports = rw;