<commit_before>/**
 * User: jcorbett
 * Date: 11/24/13
 * Time: 10:15 PM
 */

angular.module('slickApp')
    .config(['$routeProvider', 'NavigationServiceProvider', function ($routeProvider, nav) {
        $routeProvider
            .when('/testruns', {
                templateUrl: 'static/resources/pages/testruns/testrun-list.html',
                controller: 'TestrunListCtrl'
            })
            .when('/testruns/:testrunid', {
                templateUrl: 'static/resources/pages/testruns/testrun-summary.html',
                controller: 'TestrunSummaryCtrl'
            });
        nav.addLink('Reports', 'Testrun List', 'testruns');
    }])
    .controller('TestrunListCtrl', ['$scope', 'Restangular', 'NavigationService', '$routeParams', '$cookieStore', '$location', function ($scope, rest, nav, $routeParams, $cookieStore, $location) {
        $scope.project = null;
        $scope.release = null;
        $scope.testplan = null;

        $scope.projects = [];
        $scope.releases = [];
        $scope.testplans = [];

        if (!$routeParams["project"] && $cookieStore.get('slick-last-project-used')) {
            $location.search("project", $cookieStore.get('slick-last-project-used'));
        }

        nav.setTitle("Testruns");
        rest.all('projects').getList().then(function(projects) {
            $scope.projects = _.sortBy(projects, "lastUpdated");
            $scope.projects.reverse();
            if ($routeParams["project"]) {
                $scope.project = _.find(projects, function(project) {
                    return $routeParams["project"] == project.name;
                });
                $scope.$watch('project', function (newValue, oldValue) {
                    if (newValue && oldValue != newValue) {
                        $location.search("project", newValue.name);
                        $location.search("release", null);
                        $location.search("testplanId", null);
                    }
                });
                if ($scope.project) {
                    $scope.releases = $scope.project.releases;
                    if ($routeParams["release"]) {
                        $scope.release = _.find($scope.releases, function(release) {
                            return $routeParams["release"] == release.name;
                        });
                    }
                    $scope.$watch('release', function (newValue, oldValue) {
                        if(newValue) {
                            $location.search('release', newValue.name);
                        }
                    });
                    rest.all('testplans').getList({q: "eq(project.id,\"" + $scope.project.id + "\")"}).then(function(testplans) {
                        $scope.testplans = testplans;
                        if ($routeParams["testplanId"]) {
                            $scope.testplan = _.find($scope.testplans, function(testplan) {
                                return $routeParams["testplanId"] == testplan.id;
                            });
                        }
                        $scope.$watch('testplan', function(newValue, oldValue) {
                            if (newValue) {
                                $location.search("testplanId", newValue.id);
                            }
                        });
                    });
                }

            }
        });

        var testrunQuery = [];
        if ($routeParams["project"]) {
            testrunQuery.push("eq(project.name,\"" + $routeParams["project"] + "\")");
        }
        if ($routeParams["release"]) {
            testrunQuery.push("eq(release.name,\"" + $routeParams["release"] + "\")");
        }
        if ($routeParams["testplanId"]) {
            testrunQuery.push("eq(testplanId,\"" + $routeParams["testplanId"] + "\")");
        }

        var q = "";
        if (testrunQuery.length > 1) {
            q = "and(";
        }
        q = q + testrunQuery.join();
        if (testrunQuery.length > 1) {
            q = q + ")";
        }
        if (!q) {
            rest.all('testruns').getList().then(function(testruns) {
                $scope.testruns = testruns;
                window.testruns = testruns;
            });
        } else {
            rest.all('testruns').getList({q: q}).then(function(testruns) {
                $scope.testruns = testruns;
                window.testruns = testruns;
            });
        }
        $scope.testrunList = {}; // Model for the list header and filter

        $scope.getDisplayName = function(testrun) {
            var retval = testrun.name;
            if (testrun.testplan) {
                retval = testrun.testplan.name;
            }
            return retval;
        };

    }])
    .controller('TestrunSummaryCtrl', ['$scope', 'Restangular', 'NavigationService', '$routeParams', '$location', function ($scope, rest, nav, $routeParams, $location) {
        $scope.testrun = {};
        $scope.results = [];
        $scope.filter = {};
        $scope.resultQuery = {};
        $scope.resultList = {};
        $scope.moreDetailForResult = {};
        $scope.moreDetail = false;
        $scope.data = new google.visualization.DataTable();
        $scope.data.addColumn('string', 'Status');
        $scope.data.addColumn('number', 'Results');
        $scope.showAddNote = false;
        $scope.note = {
            result: null,
            message: '',
            externalLink: '',
            recurring: false,
            matchRelease: true,
            matchEnvironment: true
        };
        $scope.showDisplayFile = false;
        $scope.fileToDisplay = {};
        $scope.reason = "";
        $scope.showDisplayReason = false;
        $scope.options = {
            chartArea: {left: '5%', top: '5%', width: '90%', height: '90%'},
            backgroundColor: "#000000",
            pieSliceBorderColor: "#000000",
            legend: 'none',
            colors: []
        };
        rest.one('testruns', $routeParams["testrunid"]).get().then(function(testrun) {
            $scope.testrun = testrun;
            $scope.data = new google.visualization.DataTable();
            $scope.data.addColumn('string', 'Status');
            $scope.data.addColumn('number', 'Results');
            var emptyFilter = _.isEmpty($scope.filter);
            if (emptyFilter) {
                $scope.filter = {};
            }
            _.each(testrun.summary.statusListOrdered, function(status) {
                $scope.data.addRow([status.replace("_", " "), testrun.summary.resultsByStatus[status]]);
                $scope.options.colors.push(getStyle(status.replace("_", "") + "-element", "color"));
                if (emptyFilter) {
                    $scope.filter[status] = status != "PASS";
                }
            });

            $scope.testrunQuery();
            nav.setTitle("Summary: " + $scope.getDisplayName(testrun));
        });

        $scope.testrunQuery = function() {
            var oldQuery = $scope.resultQuery.q;
            var includableStatuses = _.filter(_.keys($scope.filter), function(key) { return $scope.filter[key]});
            if(includableStatuses.length == 1) {
                $scope.resultQuery = {
                    q: "and(eq(testrun.testrunId,\"" + $routeParams["testrunid"] + "\"),eq(status,\"" + includableStatuses[0] + "\"))",
                    allfields: "true"
                };
            } else {
                var statuses = [];
                _.each(includableStatuses, function(status) {
                    statuses.push("eq(status,\"" + status + "\")");
                });
                $scope.resultQuery = {
                    q: "and(eq(testrun.testrunId,\"" + $routeParams["testrunid"] + "\"),or(" + statuses.join(",") + "))",
                    allfields: "true"
                }
            }
            if (oldQuery != $scope.resultQuery.q) {
                rest.all('results').getList($scope.resultQuery).then(function(results) {
                    $scope.results = results;
                });
            }
        };

        $scope.getDisplayName = function(testrun) {
            var retval = testrun.name;
            if (testrun.testplan) {
                retval = testrun.testplan.name;
            }
            return retval;
        };

        $scope.getTestrunDuration = function(testrun) {
            return getDurationString(testrun.runFinished - testrun.runStarted);
        };

        $scope.getResultDuration = function(result) {
            if (result.runlength) {
                return getDurationString(result.runlength);
            }
            if (result.started && result.finished) {
                return getDurationString(result.finished - result.started);
            }
        };

        $scope.$watch('statusFilter.$dirty', function(newValue, oldValue) {
            if (newValue && (newValue != oldValue)) {
                $scope.testrunQuery();
                $scope.statusFilter.$setPristine();
            }
        });

        $scope.getAbbreviatedReason = function(result) {
            var line = result.reason.split("\n")[0];
            return line;
        };

        $scope.getImages = function(result) {
            return _.filter(result.files, function(file) { return /^image/.test(file.mimetype);});
        };

        $scope.addMoreDetail = function(result) {
            $scope.moreDetailForResult[result.id] = true;
        };

        $scope.removeDetail = function(result) {
            $scope.moreDetailForResult[result.id] = false;
        };

        $scope.addNote = function(result) {
            $scope.note.result = result;
            $scope.showAddNote = true;
        };

        $scope.addNoteDialogButtonClicked = function(buttonName) {
            if (buttonName == "Add") {
                var result = $scope.note.result;
                var logEntry = {
                    entryTime: new Date().getTime(),
                    level: "WARN",
                    loggerName: "slick.note",
                    message: $scope.note.message,
                    exceptionMessage: $scope.note.externalLink
                };
                rest.one('results', result.id).post('log',logEntry).then(function(numOfLogEntries) {
                    if (!result.log) {
                        result.log = [];
                    }
                    result.log.push(logEntry);
                });
            }
            $scope.showAddNote = false;
            $scope.note = {
                result: null,
                message: '',
                externalLink: '',
                recurring: false,
                matchRelease: true,
                matchEnvironment: true
            };
        };

        $scope.getResultNotes = function(result) {
            return _.filter(result.log, function(logEntry) {
                return logEntry.level == "WARN" && logEntry.loggerName == "slick.note";
            });
        };

        $scope.displayFile = function(file, $event) {
            $scope.fileToDisplay = file;
            $scope.showDisplayFile = true;
            $event.preventDefault();
            if(file && file.mimetype && (file.mimetype.indexOf("text") === 0 || file.mimetype == "application/xml")) {
                rest.one('files', file.id).one('content', file.filename).get().then(function(text) {
                    $scope.fileToDisplay.text = text;
                });
            }
        };

        $scope.getFileViewer = function(file) {
            if(file && file.mimetype) {
                if (file.mimetype.indexOf("image") === 0) {
                    return "image";
                } else if(file.mimetype.indexOf("video") === 0) {
                    if (file.mimetype == "video/mp4") {
                        return "html5-video";
                    } else {
                        return "embed-video";
                    }
                } else if(file.mimetype.indexOf("text") >= 0 || file.mimetype == "application/xml") {
                    return "text";
                }

            }
        };

        $scope.getUrlForFile = function(file) {
            return "api/files/" + file.id + "/content/" + file.filename;
        };

        $scope.displayFileDialogButtonClicked = function(buttonName) {
            $scope.showDisplayFile = false;
            $scope.fileToDisplay = {};
        };

        $scope.displayReason = function(result, $event) {
            $event.preventDefault();
            $scope.showDisplayReason = true;
            $scope.reason = result.reason;
        };

        $scope.displayReasonDialogButtonClicked = function(buttonName) {
            $scope.showDisplayReason = false;
            $scope.reason = "";
        };

        window.scope = $scope;
    }]);

<commit_msg>more flexible text mimetype checking<commit_after>/**
 * User: jcorbett
 * Date: 11/24/13
 * Time: 10:15 PM
 */

angular.module('slickApp')
    .config(['$routeProvider', 'NavigationServiceProvider', function ($routeProvider, nav) {
        $routeProvider
            .when('/testruns', {
                templateUrl: 'static/resources/pages/testruns/testrun-list.html',
                controller: 'TestrunListCtrl'
            })
            .when('/testruns/:testrunid', {
                templateUrl: 'static/resources/pages/testruns/testrun-summary.html',
                controller: 'TestrunSummaryCtrl'
            });
        nav.addLink('Reports', 'Testrun List', 'testruns');
    }])
    .controller('TestrunListCtrl', ['$scope', 'Restangular', 'NavigationService', '$routeParams', '$cookieStore', '$location', function ($scope, rest, nav, $routeParams, $cookieStore, $location) {
        $scope.project = null;
        $scope.release = null;
        $scope.testplan = null;

        $scope.projects = [];
        $scope.releases = [];
        $scope.testplans = [];

        if (!$routeParams["project"] && $cookieStore.get('slick-last-project-used')) {
            $location.search("project", $cookieStore.get('slick-last-project-used'));
        }

        nav.setTitle("Testruns");
        rest.all('projects').getList().then(function(projects) {
            $scope.projects = _.sortBy(projects, "lastUpdated");
            $scope.projects.reverse();
            if ($routeParams["project"]) {
                $scope.project = _.find(projects, function(project) {
                    return $routeParams["project"] == project.name;
                });
                $scope.$watch('project', function (newValue, oldValue) {
                    if (newValue && oldValue != newValue) {
                        $location.search("project", newValue.name);
                        $location.search("release", null);
                        $location.search("testplanId", null);
                    }
                });
                if ($scope.project) {
                    $scope.releases = $scope.project.releases;
                    if ($routeParams["release"]) {
                        $scope.release = _.find($scope.releases, function(release) {
                            return $routeParams["release"] == release.name;
                        });
                    }
                    $scope.$watch('release', function (newValue, oldValue) {
                        if(newValue) {
                            $location.search('release', newValue.name);
                        }
                    });
                    rest.all('testplans').getList({q: "eq(project.id,\"" + $scope.project.id + "\")"}).then(function(testplans) {
                        $scope.testplans = testplans;
                        if ($routeParams["testplanId"]) {
                            $scope.testplan = _.find($scope.testplans, function(testplan) {
                                return $routeParams["testplanId"] == testplan.id;
                            });
                        }
                        $scope.$watch('testplan', function(newValue, oldValue) {
                            if (newValue) {
                                $location.search("testplanId", newValue.id);
                            }
                        });
                    });
                }

            }
        });

        var testrunQuery = [];
        if ($routeParams["project"]) {
            testrunQuery.push("eq(project.name,\"" + $routeParams["project"] + "\")");
        }
        if ($routeParams["release"]) {
            testrunQuery.push("eq(release.name,\"" + $routeParams["release"] + "\")");
        }
        if ($routeParams["testplanId"]) {
            testrunQuery.push("eq(testplanId,\"" + $routeParams["testplanId"] + "\")");
        }

        var q = "";
        if (testrunQuery.length > 1) {
            q = "and(";
        }
        q = q + testrunQuery.join();
        if (testrunQuery.length > 1) {
            q = q + ")";
        }
        if (!q) {
            rest.all('testruns').getList().then(function(testruns) {
                $scope.testruns = testruns;
                window.testruns = testruns;
            });
        } else {
            rest.all('testruns').getList({q: q}).then(function(testruns) {
                $scope.testruns = testruns;
                window.testruns = testruns;
            });
        }
        $scope.testrunList = {}; // Model for the list header and filter

        $scope.getDisplayName = function(testrun) {
            var retval = testrun.name;
            if (testrun.testplan) {
                retval = testrun.testplan.name;
            }
            return retval;
        };

    }])
    .controller('TestrunSummaryCtrl', ['$scope', 'Restangular', 'NavigationService', '$routeParams', '$location', function ($scope, rest, nav, $routeParams, $location) {
        $scope.testrun = {};
        $scope.results = [];
        $scope.filter = {};
        $scope.resultQuery = {};
        $scope.resultList = {};
        $scope.moreDetailForResult = {};
        $scope.moreDetail = false;
        $scope.data = new google.visualization.DataTable();
        $scope.data.addColumn('string', 'Status');
        $scope.data.addColumn('number', 'Results');
        $scope.showAddNote = false;
        $scope.note = {
            result: null,
            message: '',
            externalLink: '',
            recurring: false,
            matchRelease: true,
            matchEnvironment: true
        };
        $scope.showDisplayFile = false;
        $scope.fileToDisplay = {};
        $scope.reason = "";
        $scope.showDisplayReason = false;
        $scope.options = {
            chartArea: {left: '5%', top: '5%', width: '90%', height: '90%'},
            backgroundColor: "#000000",
            pieSliceBorderColor: "#000000",
            legend: 'none',
            colors: []
        };
        rest.one('testruns', $routeParams["testrunid"]).get().then(function(testrun) {
            $scope.testrun = testrun;
            $scope.data = new google.visualization.DataTable();
            $scope.data.addColumn('string', 'Status');
            $scope.data.addColumn('number', 'Results');
            var emptyFilter = _.isEmpty($scope.filter);
            if (emptyFilter) {
                $scope.filter = {};
            }
            _.each(testrun.summary.statusListOrdered, function(status) {
                $scope.data.addRow([status.replace("_", " "), testrun.summary.resultsByStatus[status]]);
                $scope.options.colors.push(getStyle(status.replace("_", "") + "-element", "color"));
                if (emptyFilter) {
                    $scope.filter[status] = status != "PASS";
                }
            });

            $scope.testrunQuery();
            nav.setTitle("Summary: " + $scope.getDisplayName(testrun));
        });

        $scope.testrunQuery = function() {
            var oldQuery = $scope.resultQuery.q;
            var includableStatuses = _.filter(_.keys($scope.filter), function(key) { return $scope.filter[key]});
            if(includableStatuses.length == 1) {
                $scope.resultQuery = {
                    q: "and(eq(testrun.testrunId,\"" + $routeParams["testrunid"] + "\"),eq(status,\"" + includableStatuses[0] + "\"))",
                    allfields: "true"
                };
            } else {
                var statuses = [];
                _.each(includableStatuses, function(status) {
                    statuses.push("eq(status,\"" + status + "\")");
                });
                $scope.resultQuery = {
                    q: "and(eq(testrun.testrunId,\"" + $routeParams["testrunid"] + "\"),or(" + statuses.join(",") + "))",
                    allfields: "true"
                }
            }
            if (oldQuery != $scope.resultQuery.q) {
                rest.all('results').getList($scope.resultQuery).then(function(results) {
                    $scope.results = results;
                });
            }
        };

        $scope.getDisplayName = function(testrun) {
            var retval = testrun.name;
            if (testrun.testplan) {
                retval = testrun.testplan.name;
            }
            return retval;
        };

        $scope.getTestrunDuration = function(testrun) {
            return getDurationString(testrun.runFinished - testrun.runStarted);
        };

        $scope.getResultDuration = function(result) {
            if (result.runlength) {
                return getDurationString(result.runlength);
            }
            if (result.started && result.finished) {
                return getDurationString(result.finished - result.started);
            }
        };

        $scope.$watch('statusFilter.$dirty', function(newValue, oldValue) {
            if (newValue && (newValue != oldValue)) {
                $scope.testrunQuery();
                $scope.statusFilter.$setPristine();
            }
        });

        $scope.getAbbreviatedReason = function(result) {
            var line = result.reason.split("\n")[0];
            return line;
        };

        $scope.getImages = function(result) {
            return _.filter(result.files, function(file) { return /^image/.test(file.mimetype);});
        };

        $scope.addMoreDetail = function(result) {
            $scope.moreDetailForResult[result.id] = true;
        };

        $scope.removeDetail = function(result) {
            $scope.moreDetailForResult[result.id] = false;
        };

        $scope.addNote = function(result) {
            $scope.note.result = result;
            $scope.showAddNote = true;
        };

        $scope.addNoteDialogButtonClicked = function(buttonName) {
            if (buttonName == "Add") {
                var result = $scope.note.result;
                var logEntry = {
                    entryTime: new Date().getTime(),
                    level: "WARN",
                    loggerName: "slick.note",
                    message: $scope.note.message,
                    exceptionMessage: $scope.note.externalLink
                };
                rest.one('results', result.id).post('log',logEntry).then(function(numOfLogEntries) {
                    if (!result.log) {
                        result.log = [];
                    }
                    result.log.push(logEntry);
                });
            }
            $scope.showAddNote = false;
            $scope.note = {
                result: null,
                message: '',
                externalLink: '',
                recurring: false,
                matchRelease: true,
                matchEnvironment: true
            };
        };

        $scope.getResultNotes = function(result) {
            return _.filter(result.log, function(logEntry) {
                return logEntry.level == "WARN" && logEntry.loggerName == "slick.note";
            });
        };

        $scope.displayFile = function(file, $event) {
            $scope.fileToDisplay = file;
            $scope.showDisplayFile = true;
            $event.preventDefault();
            if(file && file.mimetype && (file.mimetype.indexOf("text/") >= 0 || file.mimetype == "application/xml")) {
                rest.one('files', file.id).one('content', file.filename).get().then(function(text) {
                    $scope.fileToDisplay.text = text;
                });
            }
        };

        $scope.getFileViewer = function(file) {
            if(file && file.mimetype) {
                if (file.mimetype.indexOf("image") === 0) {
                    return "image";
                } else if(file.mimetype.indexOf("video") === 0) {
                    if (file.mimetype == "video/mp4") {
                        return "html5-video";
                    } else {
                        return "embed-video";
                    }
                } else if(file.mimetype.indexOf("text/") >= 0 || file.mimetype == "application/xml") {
                    return "text";
                }

            }
        };

        $scope.getUrlForFile = function(file) {
            return "api/files/" + file.id + "/content/" + file.filename;
        };

        $scope.displayFileDialogButtonClicked = function(buttonName) {
            $scope.showDisplayFile = false;
            $scope.fileToDisplay = {};
        };

        $scope.displayReason = function(result, $event) {
            $event.preventDefault();
            $scope.showDisplayReason = true;
            $scope.reason = result.reason;
        };

        $scope.displayReasonDialogButtonClicked = function(buttonName) {
            $scope.showDisplayReason = false;
            $scope.reason = "";
        };

        window.scope = $scope;
    }]);

<|endoftext|>


<commit_before>/*    Copyright 2019 Firewalla INC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const Sensor = require('./Sensor.js').Sensor;
const timeSeries = require('../util/TimeSeries.js').getTimeSeries()
const HostManager = require("../net2/HostManager.js");
const hostManager = new HostManager("cli", 'client', 'info');
const util = require('util');
const getHitsAsync = util.promisify(timeSeries.getHits).bind(timeSeries);
const FlowAggrTool = require('../net2/FlowAggrTool');
const flowAggrTool = new FlowAggrTool();
const FlowTool = require('../net2/FlowTool');
const flowTool = new FlowTool();
const Alarm = require('../alarm/Alarm.js');
const AlarmManager2 = require('../alarm/AlarmManager2.js');
const alarmManager2 = new AlarmManager2();
const abnormalBandwidthUsageFeatureName = 'abnormal_bandwidth_usage';
const dataPlanFeatureName = 'data_plan';
const rclient = require('../util/redis_manager.js').getRedisClient();
const fc = require('../net2/config.js');
class DataUsageSensor extends Sensor {
    constructor() {
        super();
    }
    run() {
        this.refreshInterval = (this.config.refreshInterval || 15) * 60 * 1000;
        this.ratio = this.config.ratio || 1.2;
        this.analytics_hours = this.config.analytics_hours || 8;
        this.percentage = this.config.percentage || 0.8;
        this.topXflows = this.config.topXflows || 10;
        this.minsize = this.config.minsize || 150 * 1000 * 1000;
        this.smWindow = this.config.smWindow || 2;
        this.mdWindow = this.config.mdWindow || 8;
        this.dataPlanMinPercentage = this.config.dataPlanMinPercentage || 0.8;
        this.slot = 4// 1hour 4 slots
        this.hookFeature();
    }
    job() {
        fc.isFeatureOn(abnormalBandwidthUsageFeatureName) && this.checkDataUsage()
        fc.isFeatureOn(dataPlanFeatureName) && this.checkMonthlyDataUsage()
    }
    globalOn() {
    }

    globalOff() {
    }
    async checkDataUsage() {
        log.info("Start check data usage")
        let hosts = await hostManager.getHostsAsync();
        const systemDataUsage = await this.getTimewindowDataUsage(0, '');
        const systemRecentlyTotalUsage = this.getRecentlyDataUsage(systemDataUsage, this.smWindow * this.slot)
        hosts = hosts.filter(x => x)
        for (const host of hosts) {
            const mac = host.o.mac;
            const dataUsage = await this.getTimewindowDataUsage(0, mac);
            const dataUsageSmHourWindow = await this.getTimewindowDataUsage(this.smWindow, mac);
            const dataUsageMdHourWindow = await this.getTimewindowDataUsage(this.mdWindow, mac);
            const hostRecentlyTotalUsage = this.getRecentlyDataUsage(dataUsage, this.smWindow * this.slot)
            const hostDataUsagePercentage = hostRecentlyTotalUsage / systemRecentlyTotalUsage || 0;
            const end = dataUsage[dataUsage.length - 1].ts;
            const begin = end - this.smWindow * 60 * 60;
            const steps = this.smWindow * this.slot;
            const length = dataUsageSmHourWindow.length;
            if (hostRecentlyTotalUsage < steps * this.minsize || hostDataUsagePercentage < this.percentage) continue;
            for (let i = 1; i <= steps; i++) {
                const smUsage = dataUsageSmHourWindow[length - i].count,
                    mdUsage = dataUsageMdHourWindow[length - i].count;
                if (smUsage > this.minsize && mdUsage > this.minsize && smUsage > mdUsage) {
                    const ratio = smUsage / mdUsage;
                    if (ratio > this.ratio) {
                        this.genAbnormalBandwidthUsageAlarm(host, begin, end, hostRecentlyTotalUsage, dataUsage, hostDataUsagePercentage);
                        break;
                    }
                }
            }
        }
    }
    async getTimewindowDataUsage(timeWindow, mac) {
        const downloadKey = `download${mac ? ':' + mac : ''}`;
        const uploadKey = `upload${mac ? ':' + mac : ''}`;
        //[[ts,Bytes]]  [[1574325720, 9396810],[ 1574325780, 3141018 ]]
        const slot = this.slot;
        const slots = slot * timeWindow || 1;
        const sumSlots = (slots + 1) * (slots / 2);
        const analytics_slots = slot * this.analytics_hours + slots
        const downloadStats = await getHitsAsync(downloadKey, "15minutes", analytics_slots);
        const uploadStats = await getHitsAsync(uploadKey, "15minutes", analytics_slots);
        let dataUsageTimeWindow = [];
        if (downloadStats.length < slots) return;
        for (let i = slots; i < downloadStats.length; i++) {
            let temp = {
                count: 0,
                ts: downloadStats[i][0]
            };
            for (let j = i - slots + 1; j <= i; j++) {
                const weight = (slots - (i - j)) / sumSlots;
                temp.count = temp.count * 1 + (downloadStats[j][1] * 1 + uploadStats[j][1] * 1) * weight;
            }
            dataUsageTimeWindow.push(temp);
        }
        return dataUsageTimeWindow
    }
    getRecentlyDataUsage(data, steps) {
        const length = data.length;
        let total = 0;
        for (let i = 1; i <= steps; i++) {
            if (data[length - i] && data[length - i].count) {
                total = total * 1 + data[length - i].count * 1;
            }
        }
        return total;
    }
    async genAbnormalBandwidthUsageAlarm(host, begin, end, totalUsage, dataUsage, percentage) {
        log.info("genAbnormalBandwidthUsageAlarm", host.o.mac, begin, end)
        //get top flows from begin to end
        const mac = host.o.mac;
        const name = host.o.name || host.o.bname;
        const flows = await this.getSumFlows(mac, begin, end);
        const destNames = flows.map((flow) => flow.aggregationHost).join(',')
        percentage = percentage * 100;
        let alarm = new Alarm.AbnormalBandwidthUsageAlarm(new Date() / 1000, name, {
            "p.device.mac": mac,
            "p.device.id": name,
            "p.device.name": name,
            "p.device.ip": host.o.ipv4Addr,
            "p.totalUsage": totalUsage,
            "p.begin.ts": begin,
            "p.end.ts": end,
            "e.transfers": dataUsage,
            "p.flows": JSON.stringify(flows),
            "p.dest.names": destNames,
            "p.duration": this.smWindow,
            "p.percentage": percentage.toFixed(2) + '%'
        });
        await alarmManager2.enqueueAlarm(alarm);
    }
    async getSumFlows(mac, begin, end) {
        // hourly sum
        const period = 60 * 60;
        begin = begin - begin % period;
        end = end - end % period + period;
        let flows = [];
        while (begin < end) {
            const sumDownloadFlowKey = flowAggrTool.getSumFlowKey(mac, 'download', begin, begin + period);
            const downloadTraffics = await flowAggrTool.getTopSumFlowByKey(sumDownloadFlowKey, 10);//get top 10 flows
            const sumUploadFlowKey = flowAggrTool.getSumFlowKey(mac, 'upload', begin, begin + period);
            const uploadTraffics = await flowAggrTool.getTopSumFlowByKey(sumUploadFlowKey, 10);
            flows = flows.concat(downloadTraffics).concat(uploadTraffics);
            begin = begin + period;
        }
        flows = await flowTool.enrichWithIntel(flows);
        let flowsCache = {};
        for (const flow of flows) {
            const destHost = flow.host ? flow.host.split('.').slice(-2).join('.') : flow.ip;
            if (flowsCache[destHost]) {
                flowsCache[destHost].count = flowsCache[destHost].count * 1 + flow.count * 1;
            } else {
                flowsCache[destHost] = flow
            }
        }
        let flowsGroupByDestHost = [];
        for (const destHost in flowsCache) {
            flowsCache[destHost].aggregationHost = destHost;
            flowsGroupByDestHost.push(flowsCache[destHost]);
        }
        return flowsGroupByDestHost.sort((a, b) => b.count * 1 - a.count * 1).splice(0, this.topXflows).filter((flow) => {
            return flow.count * 1 > 10 * 1000 * 1000;//return flows bigger than 10MB
        })
    }
    async checkMonthlyDataUsage() {
        log.info("Start check monthly data usage")
        let dataPlan = await rclient.getAsync('sys:data:plan');
        if (!dataPlan) return;
        dataPlan = JSON.parse(dataPlan);
        const { date, total } = dataPlan;
        const { totalDownload, totalUpload, monthlyBeginTs, monthlyEndTs } = await hostManager.monthlyDataStats(null, date);
        let percentage = ((totalDownload + totalUpload) / total)
        if (percentage >= this.dataPlanMinPercentage) {
            //gen over data plan alarm
            const level = Math.floor(percentage * 10);
            percentage = percentage * 100;
            let alarm = new Alarm.OverDataPlanUsageAlarm(new Date() / 1000, null, {
                "p.monthly.endts": monthlyEndTs,
                "p.percentage": percentage.toFixed(2) + '%',
                "p.totalUsage": totalDownload + totalUpload,
                "p.planUsage": total,
                "p.alarm.level": level >= 10 ? 'over' : level
            });
            await alarmManager2.enqueueAlarm(alarm);
        }
    }
}

module.exports = DataUsageSensor;
<commit_msg>update<commit_after>/*    Copyright 2019 Firewalla INC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const Sensor = require('./Sensor.js').Sensor;
const timeSeries = require('../util/TimeSeries.js').getTimeSeries()
const HostManager = require("../net2/HostManager.js");
const hostManager = new HostManager("cli", 'client', 'info');
const util = require('util');
const getHitsAsync = util.promisify(timeSeries.getHits).bind(timeSeries);
const FlowAggrTool = require('../net2/FlowAggrTool');
const flowAggrTool = new FlowAggrTool();
const FlowTool = require('../net2/FlowTool');
const flowTool = new FlowTool();
const Alarm = require('../alarm/Alarm.js');
const AlarmManager2 = require('../alarm/AlarmManager2.js');
const alarmManager2 = new AlarmManager2();
const abnormalBandwidthUsageFeatureName = 'abnormal_bandwidth_usage';
const dataPlanFeatureName = 'data_plan';
const rclient = require('../util/redis_manager.js').getRedisClient();
const fc = require('../net2/config.js');
class DataUsageSensor extends Sensor {
    constructor() {
        super();
    }
    run() {
        this.refreshInterval = (this.config.refreshInterval || 15) * 60 * 1000;
        this.ratio = this.config.ratio || 1.2;
        this.analytics_hours = this.config.analytics_hours || 8;
        this.percentage = this.config.percentage || 0.8;
        this.topXflows = this.config.topXflows || 10;
        this.minsize = this.config.minsize || 150 * 1000 * 1000;
        this.smWindow = this.config.smWindow || 2;
        this.mdWindow = this.config.mdWindow || 8;
        this.dataPlanMinPercentage = this.config.dataPlanMinPercentage || 0.8;
        this.slot = 4// 1hour 4 slots
        this.hookFeature();
    }
    job() {
        fc.isFeatureOn(abnormalBandwidthUsageFeatureName) && this.checkDataUsage()
        fc.isFeatureOn(dataPlanFeatureName) && this.checkMonthlyDataUsage()
    }
    globalOn() {
    }

    globalOff() {
    }
    async checkDataUsage() {
        log.info("Start check data usage")
        let hosts = await hostManager.getHostsAsync();
        const systemDataUsage = await this.getTimewindowDataUsage(0, '');
        const systemRecentlyTotalUsage = this.getRecentlyDataUsage(systemDataUsage, this.smWindow * this.slot)
        hosts = hosts.filter(x => x)
        for (const host of hosts) {
            const mac = host.o.mac;
            const dataUsage = await this.getTimewindowDataUsage(0, mac);
            const dataUsageSmHourWindow = await this.getTimewindowDataUsage(this.smWindow, mac);
            const dataUsageMdHourWindow = await this.getTimewindowDataUsage(this.mdWindow, mac);
            const hostRecentlyTotalUsage = this.getRecentlyDataUsage(dataUsage, this.smWindow * this.slot)
            const hostDataUsagePercentage = hostRecentlyTotalUsage / systemRecentlyTotalUsage || 0;
            const end = dataUsage[dataUsage.length - 1].ts;
            const begin = end - this.smWindow * 60 * 60;
            const steps = this.smWindow * this.slot;
            const length = dataUsageSmHourWindow.length;
            if (hostRecentlyTotalUsage < steps * this.minsize || hostDataUsagePercentage < this.percentage) continue;
            for (let i = 1; i <= steps; i++) {
                const smUsage = dataUsageSmHourWindow[length - i].count,
                    mdUsage = dataUsageMdHourWindow[length - i].count;
                if (smUsage > this.minsize && mdUsage > this.minsize && smUsage > mdUsage) {
                    const ratio = smUsage / mdUsage;
                    if (ratio > this.ratio) {
                        this.genAbnormalBandwidthUsageAlarm(host, begin, end, hostRecentlyTotalUsage, dataUsage, hostDataUsagePercentage);
                        break;
                    }
                }
            }
        }
    }
    async getTimewindowDataUsage(timeWindow, mac) {
        const downloadKey = `download${mac ? ':' + mac : ''}`;
        const uploadKey = `upload${mac ? ':' + mac : ''}`;
        //[[ts,Bytes]]  [[1574325720, 9396810],[ 1574325780, 3141018 ]]
        const slot = this.slot;
        const slots = slot * timeWindow || 1;
        const sumSlots = (slots + 1) * (slots / 2);
        const analytics_slots = slot * this.analytics_hours + slots
        const downloadStats = await getHitsAsync(downloadKey, "15minutes", analytics_slots);
        const uploadStats = await getHitsAsync(uploadKey, "15minutes", analytics_slots);
        let dataUsageTimeWindow = [];
        if (downloadStats.length < slots) return;
        for (let i = slots; i < downloadStats.length; i++) {
            let temp = {
                count: 0,
                ts: downloadStats[i][0]
            };
            for (let j = i - slots + 1; j <= i; j++) {
                const weight = (slots - (i - j)) / sumSlots;
                temp.count = temp.count * 1 + (downloadStats[j][1] * 1 + uploadStats[j][1] * 1) * weight;
            }
            dataUsageTimeWindow.push(temp);
        }
        return dataUsageTimeWindow
    }
    getRecentlyDataUsage(data, steps) {
        const length = data.length;
        let total = 0;
        for (let i = 1; i <= steps; i++) {
            if (data[length - i] && data[length - i].count) {
                total = total * 1 + data[length - i].count * 1;
            }
        }
        return total;
    }
    async genAbnormalBandwidthUsageAlarm(host, begin, end, totalUsage, dataUsage, percentage) {
        log.info("genAbnormalBandwidthUsageAlarm", host.o.mac, begin, end)
        //get top flows from begin to end
        const mac = host.o.mac;
        const name = host.o.name || host.o.bname;
        const flows = await this.getSumFlows(mac, begin, end);
        const destNames = flows.map((flow) => flow.aggregationHost).join(',')
        percentage = percentage * 100;
        const last24HoursDownloadStats = await getHitsAsync(`download:${mac}`, "15minutes", this.slot * 24)
        const last24HoursUploadStats = await getHitsAsync(`upload:${mac}`, "15minutes", this.slot * 24)
        const recentlyDownloadStats = await getHitsAsync(`download:${mac}`, "15minutes", this.slot * this.smWindow)
        const recentlyUploadStats = await getHitsAsync(`download:${mac}`, "15minutes", this.slot * this.smWindow)
        const last24HoursStats = {
            download: last24HoursDownloadStats,
            upload: last24HoursUploadStats
        }
        const recentlyStats = {
            download: recentlyDownloadStats,
            upload: recentlyUploadStats
        }
        let alarm = new Alarm.AbnormalBandwidthUsageAlarm(new Date() / 1000, name, {
            "p.device.mac": mac,
            "p.device.id": name,
            "p.device.name": name,
            "p.device.ip": host.o.ipv4Addr,
            "p.totalUsage": totalUsage,
            "p.begin.ts": begin,
            "p.end.ts": end,
            "e.transfers": recentlyStats,
            "e.last24.transfers": last24HoursStats,
            "p.flows": JSON.stringify(flows),
            "p.dest.names": destNames,
            "p.duration": this.smWindow,
            "p.percentage": percentage.toFixed(2) + '%'
        });
        await alarmManager2.enqueueAlarm(alarm);
    }
    async getSumFlows(mac, begin, end) {
        // hourly sum
        const period = 60 * 60;
        begin = begin - begin % period;
        end = end - end % period + period;
        let flows = [];
        while (begin < end) {
            const sumDownloadFlowKey = flowAggrTool.getSumFlowKey(mac, 'download', begin, begin + period);
            const downloadTraffics = await flowAggrTool.getTopSumFlowByKey(sumDownloadFlowKey, 10);//get top 10 flows
            const sumUploadFlowKey = flowAggrTool.getSumFlowKey(mac, 'upload', begin, begin + period);
            const uploadTraffics = await flowAggrTool.getTopSumFlowByKey(sumUploadFlowKey, 10);
            flows = flows.concat(downloadTraffics).concat(uploadTraffics);
            begin = begin + period;
        }
        flows = await flowTool.enrichWithIntel(flows);
        let flowsCache = {};
        for (const flow of flows) {
            const destHost = flow.host ? flow.host.split('.').slice(-2).join('.') : flow.ip;
            if (flowsCache[destHost]) {
                flowsCache[destHost].count = flowsCache[destHost].count * 1 + flow.count * 1;
            } else {
                flowsCache[destHost] = flow
            }
        }
        let flowsGroupByDestHost = [];
        for (const destHost in flowsCache) {
            flowsCache[destHost].aggregationHost = destHost;
            flowsGroupByDestHost.push(flowsCache[destHost]);
        }
        return flowsGroupByDestHost.sort((a, b) => b.count * 1 - a.count * 1).splice(0, this.topXflows).filter((flow) => {
            return flow.count * 1 > 10 * 1000 * 1000;//return flows bigger than 10MB
        })
    }
    async checkMonthlyDataUsage() {
        log.info("Start check monthly data usage")
        let dataPlan = await rclient.getAsync('sys:data:plan');
        if (!dataPlan) return;
        dataPlan = JSON.parse(dataPlan);
        const { date, total } = dataPlan;
        const { totalDownload, totalUpload, monthlyBeginTs, monthlyEndTs } = await hostManager.monthlyDataStats(null, date);
        let percentage = ((totalDownload + totalUpload) / total)
        if (percentage >= this.dataPlanMinPercentage) {
            //gen over data plan alarm
            const level = Math.floor(percentage * 10);
            percentage = percentage * 100;
            let alarm = new Alarm.OverDataPlanUsageAlarm(new Date() / 1000, null, {
                "p.monthly.endts": monthlyEndTs,
                "p.percentage": percentage.toFixed(2) + '%',
                "p.totalUsage": totalDownload + totalUpload,
                "p.planUsage": total,
                "p.alarm.level": level >= 10 ? 'over' : level
            });
            await alarmManager2.enqueueAlarm(alarm);
        }
    }
}

module.exports = DataUsageSensor;
<|endoftext|>


<commit_before>// Parent class for asset views.
var ParentAssetView = Backbone.View.extend({
    /* Parent class for the asset views.
     *  - initialize()
     *  - render()
     *  - derender()
     */
    initialize: function() {
        _.bindAll(this, 'render', 'derender');
    },
    render: function() {
        if (this.model) { this.$el.html(this.template(this.model.toJSON())) } else { this.$el.html(this.template())};
        return this;
    },
    derender: function() {
        this.remove();
        this.unbind();
        this.model.off;
    }
});

//Header for the asset table.
var AssetTableHeaderView = ParentAssetView.extend({
    /* This will render the search field, pagination boxes,
     * and the 'create asset' link button.
     * - initialize()
     * - createAsset()
     */
    template: JST['ooiui/static/js/partials/AssetTableHeader.html'],
    initialize: function() {
        _.bindAll(this, 'createAsset');
    },
    events: {
        "click #assetCreatorBtn" : "createAsset"
    },
    createAsset: function() {
        var assetCreatorModal = new AssetCreatorModalView();
        $(assetCreatorModal.render().el).appendTo('#assetCreatorModal');
        assetCreatorModal.setupFields();
        return this;
    }

});

//Container for the list of all assets.
var AssetsTableView = ParentAssetView.extend({
    /* This is the meat and potatoes of the page;
     * all the assets in the collection are rendered
     * out through this view.
     * - render()
     */
    tagName: 'tbody',
    render: function() {
        var assetsRowView = this.collection.map(function(model) {
            return (new AssetsTableRowView({ model:model })).render().el;
        });
        this.$el.html(assetsRowView);
        return this;
    }
});

//Asset item subview, as a table row.
var AssetsTableRowView = ParentAssetView.extend({
    /* Each asset is itemized in it's own row for display.
     * - initialize()
     * - renderSubViews()
     */
    tagName: 'tr',
    template: JST['ooiui/static/js/partials/AssetsTableRow.html'],
    initialize: function() {
        _.bindAll(this, 'renderSubViews');
        this.listenToOnce(vent, 'asset:tableDerender', function(model) {
            this.derender();
        });
        this.listenTo(vent, 'asset:modelChange', this.render);
    },
    events: {
        "click" : "renderSubViews"
    },
    renderSubViews: function() {
        $('#assetDetailsPlaceholder').remove();
        vent.trigger('asset:renderSubViews', this.model);
    }
});

//Asset inspector panel.
var AssetInspectorView = ParentAssetView.extend({
    /* This will display the details of the asset after it's row item
     * has been clicked.  It will live below the list of assets, on the left.
     * It contains 3 tabs, 'Overview', 'Meta Data', and 'Aquision Data'.
     * This view also constructs the 'Edit Asset' button, and renders own the
     * view associated.
     * - initialize()
     * - editAsset()
     */
    template: JST['ooiui/static/js/partials/AssetInspector.html'],
    initialize: function() {
        _.bindAll(this, 'editAsset');
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
        this.listenTo(vent, 'asset:modelChange', this.render);
    },
    events: {
        "click #assetEditorBtn": "editAsset",
    },
    editAsset: function() {
        var assetEditorModal = new AssetEditorModalView({ model:this.model });
        $(assetEditorModal.render().el).appendTo('#assetEditorModal');
        return this;
    }
});

//Asset event detail view.
var AssetEventsTableView = ParentAssetView.extend({
    /* This view lists out all the events associated with the asset.
     * A special note: when clicking the Calibration Event, the calibration
     * coeffcients will display.
     * - initialize()
     */
    template: JST['ooiui/static/js/partials/AssetEventsTable.html'],
    initialize: function() {
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
    }
});

//Asset Attachments View (nuxeo).
var AssetAttachmentsTableView = ParentAssetView.extend({
    /* TODO: enable this to interact with the NUXEO services.
     * This will display a table list of all attachements
     * associated with this asset.
     * WORK IN PROGRESS
     */
    template: JST['ooiui/static/js/partials/AssetAttachmentsTable.html'],
    initialize: function() {
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
    },
});

//Asset creator modal view
var AssetCreatorModalView = ParentAssetView.extend({
    /* Renders out a modal window, which contains a form for
     * creating a new asset.
     * TODO: create field validations for each of the inputs.
     * - initialize()
     * - setupFields() ... this is useful, if there are modifications
     *      that need to be done to the template after it's rendered.
     * - save()
     * - cancel()
     * - cleanup()
     */
    template: JST['ooiui/static/js/partials/AssetCreatorModal.html'],
    initialize: function() {
        _.bindAll(this, 'save', 'cancel', 'setupFields', 'validateFields');
    },
    events: {
        "click button#cancel" : "cancel",
        "click button#save" : "save"
    },
    setupFields: function() {
        this.$el.find('#assetLaunchDate').datepicker();
    },
    validateFields: function() {
        /* TODO:
         * 1. assetInfo: Required (all)
         *      a. name: Valid Char ( - , A-Z , a-z , 0-9 )
         *      b. owner: Valid Char ( - , A-Z , a-z , 0-9 )
         *      c. description: Valid Char ( - , A-Z , a-z , 0-9 )
         *      d. type: Selected
         * 2. manufactureInfo: Optional (all)
         *      a. manufacturer: Valid Char ( - , A-Z , a-z )
         *      b. modelNumber:  Valid Char ( - , A-Z , 0-9 )
         *      c. serialNumber:  Valid Char ( - , A-Z , 0-9)
         * 3. metaData: Required (all)
         *      a. Ref Des: Valid Char ( - , A-Z , 0-9 )
         *      b. Anchor Launch Date: Not future.
         *      c. Anchor Launch Time: 24hr format, not future.
         *      d. Latitude: Decimal Degree, DegreeMinutes, Degree Minutes Seconds
         *      e. Longitutde: Decimal Degree, DegreeMinutes, Degree Minutes Seconds
         *          - Actually map the input and return approx location
         *            below input fields (non modifiable text field).
         *      f. Water Depth: Valid Char ( m, 0-9 )
         * 4. assetClassCode: String Length, Valid Char (A-Z, 0-9)
         * 5. assetNotes: Valid Char (A-Z, a-z, 0-9, . , (comma) , - )
         * 6. purchaseAndDeliveryInfo: Valid Char (A-Z, a-z, 0-9, . , (comma) , - , $ )
         * 7. assetClass: Selected
         * 8. assetSeriesClassification: <Unknown> ... leave out for now.
         *
         * Once Complete, change 'events:'
         *  from:
         *      "click button#save" : "save"
         *  to:
         *      "click button#save" : "validate",
         *
         * Then call this.save() at the successful validation.
         */
    },
    save: function() {
        // TODO: This entire function should be called
        // on a successful form validation.  A refactor will be
        // required to implement.
        var assetInfo = {};
        assetInfo.name = this.$el.find('#assetName').val();
        assetInfo.owner = this.$el.find('#assetOwner').val();
        assetInfo.description = this.$el.find('#assetDescription').val();
        assetInfo.type = this.$el.find('#assetType').val();

        // For now, this dict isn't implemented due to issues with uframe's
        // acceptance of this data.
        var manufactureInfo = {};
        manufactureInfo.manufacturer = this.$el.find('#assetManufacturer').val();
        manufactureInfo.modelNumber = this.$el.find('#assetModelNumber').val();
        manufactureInfo.serialNumber = this.$el.find('#assetSerialNumber').val();

        // The metaData field is very loosly defined.  These are the only
        // field supported for asset creation at this time.
        var metaData = [
        {
            "key": "Ref Des",
            "value": this.$el.find('#assetRefDes').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Anchor Launch Date",
            "value": this.$el.find('#assetLaunchDate').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Anchor Launch Time",
            "value": this.$el.find('#assetLaunchTime').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Latitude",
            "value": this.$el.find('#assetLatitude').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Longitude",
            "value": this.$el.find('#assetLongitude').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Water Depth",
            "value": this.$el.find('#assetDepth').val(),
            "type": "java.lang.String"
        }];

        // Create the new asset model that will be saved to the collection,
        // and posted to the server.
        var newAsset = new AssetModel({});
        newAsset.set('assetInfo', assetInfo);
        newAsset.set('asset_class', this.$el.find('#assetClass').val());
        newAsset.set('manufactureInfo', manufactureInfo);
        newAsset.set('notes', [ this.$el.find('#assetNotes').val() ]);
        newAsset.set('purchaseAndDeliveryInfo', this.$el.find('#assetPurchaseAndDeliveryInfo').val());
        newAsset.set('metaData', metaData);
        newAsset.set('classCode', this.$el.find('#assetClassCode').val());
        newAsset.set('seriesClassification', this.$el.find('#assetSeriesClassification').val());
        console.log(newAsset);
        newAsset.save({
            success: function(){
                vent.trigger('asset:changeCollection');
            }
        });
        this.cleanUp();
        newAsset.off();
    },
    cancel: function() {
        this.cleanUp();
    },
    cleanUp: function() {
        $('#assetCreatorModal').modal('hide');
        $('body').removeClass('modal-open');
        $('.modal-backdrop').remove();
        this.remove();
        this.unbind();
    }
});
//Asset editor modal view.
var AssetEditorModalView = ParentAssetView.extend({
    /* This will render out a asset editor modal window.
     * Only certain fields are editable to date, noteably
     * the fields omitted are the reference designator.
     * - initialize()
     * - submit()
     * - cancel()
     * - destory() ... This will delete an asset...beware!
     * - cleanUp()
     */
    template: JST['ooiui/static/js/partials/AssetEditorModal.html'],
    initialize: function() {
        _.bindAll(this, 'cancel', 'submit', 'destroy', 'validateFields');
    },
    events: {
        "click button#cancelEdit" : "cancel",
        "click button#saveEdit" : "submit",
        "click button#delete" : "destroy"
    },
    validateFields: function() {

    },
    submit: function() {
        var assetInfo = this.model.get('assetInfo');
        assetInfo.name = this.$el.find('#assetName').val();
        assetInfo.owner = this.$el.find('#assetOwner').val(),
        assetInfo.description = this.$el.find('#assetDescription').val();
        assetInfo.type = this.$el.find('#assetType').val();

        this.model.set('notes', [ this.$el.find('#assetNotes').val() ]);
        this.model.set('asset_class', this.$el.find('#assetClass').val());
        this.model.set('classCode', this.$el.find('#assetClassCode').val());
        this.model.set('assetInfo', assetInfo);
        this.model.save({
            success: function(){
                this.model.fetch();
                vent.trigger('asset:modelChange', this.model);
            }
        });
        this.cleanUp();
    },
    cancel: function() {
        this.cleanUp();
    },
    destroy: function() {
        var youSure = confirm("Are you sure you would like to delete this asset?");
        if (youSure) {
            this.model.destroy();
            vent.trigger('asset:derender');
            vent.trigger('asset:changeCollection');
            this.cleanUp();
        }
    },
    cleanUp: function() {
        $('#assetEditorModal').modal('hide');
        $('body').removeClass('modal-open');
        $('.modal-backdrop').remove();
        this.derender();
    }
});
<commit_msg>Bug Fix: Editing an asset now checked properly that the asset model is up to date.<commit_after>// Parent class for asset views.
var ParentAssetView = Backbone.View.extend({
    /* Parent class for the asset views.
     *  - initialize()
     *  - render()
     *  - derender()
     */
    initialize: function() {
        _.bindAll(this, 'render', 'derender');
    },
    render: function() {
        if (this.model) { this.$el.html(this.template(this.model.toJSON())) } else { this.$el.html(this.template())};
        return this;
    },
    derender: function() {
        this.remove();
        this.unbind();
        this.model.off;
    }
});

//Header for the asset table.
var AssetTableHeaderView = ParentAssetView.extend({
    /* This will render the search field, pagination boxes,
     * and the 'create asset' link button.
     * - initialize()
     * - createAsset()
     */
    template: JST['ooiui/static/js/partials/AssetTableHeader.html'],
    initialize: function() {
        _.bindAll(this, 'createAsset');
    },
    events: {
        "click #assetCreatorBtn" : "createAsset"
    },
    createAsset: function() {
        var assetCreatorModal = new AssetCreatorModalView();
        $(assetCreatorModal.render().el).appendTo('#assetCreatorModal');
        assetCreatorModal.setupFields();
        return this;
    }

});

//Container for the list of all assets.
var AssetsTableView = ParentAssetView.extend({
    /* This is the meat and potatoes of the page;
     * all the assets in the collection are rendered
     * out through this view.
     * - render()
     */
    tagName: 'tbody',
    render: function() {
        var assetsRowView = this.collection.map(function(model) {
            return (new AssetsTableRowView({ model:model })).render().el;
        });
        this.$el.html(assetsRowView);
        return this;
    }
});

//Asset item subview, as a table row.
var AssetsTableRowView = ParentAssetView.extend({
    /* Each asset is itemized in it's own row for display.
     * - initialize()
     * - renderSubViews()
     */
    tagName: 'tr',
    template: JST['ooiui/static/js/partials/AssetsTableRow.html'],
    initialize: function() {
        _.bindAll(this, 'renderSubViews');
        this.listenToOnce(vent, 'asset:tableDerender', function(model) {
            this.derender();
        });
        this.listenTo(vent, 'asset:modelChange', this.render);
    },
    events: {
        "click" : "renderSubViews"
    },
    renderSubViews: function() {
        $('#assetDetailsPlaceholder').remove();
        vent.trigger('asset:renderSubViews', this.model);
    }
});

//Asset inspector panel.
var AssetInspectorView = ParentAssetView.extend({
    /* This will display the details of the asset after it's row item
     * has been clicked.  It will live below the list of assets, on the left.
     * It contains 3 tabs, 'Overview', 'Meta Data', and 'Aquision Data'.
     * This view also constructs the 'Edit Asset' button, and renders own the
     * view associated.
     * - initialize()
     * - editAsset()
     */
    template: JST['ooiui/static/js/partials/AssetInspector.html'],
    initialize: function() {
        _.bindAll(this, 'editAsset');
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
        this.listenTo(vent, 'asset:modelChange', this.render);
    },
    events: {
        "click #assetEditorBtn": "editAsset",
    },
    editAsset: function() {
        // before editing the asset, make sure it's up to date.
        this.model.fetch();
        var assetEditorModal = new AssetEditorModalView({ model:this.model });
        $(assetEditorModal.render().el).appendTo('#assetEditorModal');
        return this;
    }
});

//Asset event detail view.
var AssetEventsTableView = ParentAssetView.extend({
    /* This view lists out all the events associated with the asset.
     * A special note: when clicking the Calibration Event, the calibration
     * coeffcients will display.
     * - initialize()
     */
    template: JST['ooiui/static/js/partials/AssetEventsTable.html'],
    initialize: function() {
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
    }
});

//Asset Attachments View (nuxeo).
var AssetAttachmentsTableView = ParentAssetView.extend({
    /* TODO: enable this to interact with the NUXEO services.
     * This will display a table list of all attachements
     * associated with this asset.
     * WORK IN PROGRESS
     */
    template: JST['ooiui/static/js/partials/AssetAttachmentsTable.html'],
    initialize: function() {
        this.listenToOnce(vent, 'asset:derender', function(model) {
            this.derender();
        });
    },
});

//Asset creator modal view
var AssetCreatorModalView = ParentAssetView.extend({
    /* Renders out a modal window, which contains a form for
     * creating a new asset.
     * TODO: create field validations for each of the inputs.
     * - initialize()
     * - setupFields() ... this is useful, if there are modifications
     *      that need to be done to the template after it's rendered.
     * - save()
     * - cancel()
     * - cleanup()
     */
    template: JST['ooiui/static/js/partials/AssetCreatorModal.html'],
    initialize: function() {
        _.bindAll(this, 'save', 'cancel', 'setupFields', 'validateFields');
    },
    events: {
        "click button#cancel" : "cancel",
        "click button#save" : "save"
    },
    setupFields: function() {
        this.$el.find('#assetLaunchDate').datepicker();
    },
    validateFields: function() {
        /* TODO:
         * 1. assetInfo: Required (all)
         *      a. name: Valid Char ( - , A-Z , a-z , 0-9 )
         *      b. owner: Valid Char ( - , A-Z , a-z , 0-9 )
         *      c. description: Valid Char ( - , A-Z , a-z , 0-9 )
         *      d. type: Selected
         * 2. manufactureInfo: Optional (all)
         *      a. manufacturer: Valid Char ( - , A-Z , a-z )
         *      b. modelNumber:  Valid Char ( - , A-Z , 0-9 )
         *      c. serialNumber:  Valid Char ( - , A-Z , 0-9)
         * 3. metaData: Required (all)
         *      a. Ref Des: Valid Char ( - , A-Z , 0-9 )
         *      b. Anchor Launch Date: Not future.
         *      c. Anchor Launch Time: 24hr format, not future.
         *      d. Latitude: Decimal Degree, DegreeMinutes, Degree Minutes Seconds
         *      e. Longitutde: Decimal Degree, DegreeMinutes, Degree Minutes Seconds
         *          - Actually map the input and return approx location
         *            below input fields (non modifiable text field).
         *      f. Water Depth: Valid Char ( m, 0-9 )
         * 4. assetClassCode: String Length, Valid Char (A-Z, 0-9)
         * 5. assetNotes: Valid Char (A-Z, a-z, 0-9, . , (comma) , - )
         * 6. purchaseAndDeliveryInfo: Valid Char (A-Z, a-z, 0-9, . , (comma) , - , $ )
         * 7. assetClass: Selected
         * 8. assetSeriesClassification: <Unknown> ... leave out for now.
         *
         * Once Complete, change 'events:'
         *  from:
         *      "click button#save" : "save"
         *  to:
         *      "click button#save" : "validate",
         *
         * Then call this.save() at the successful validation.
         */
    },
    save: function() {
        // TODO: This entire function should be called
        // on a successful form validation.  A refactor will be
        // required to implement.
        var assetInfo = {};
        assetInfo.name = this.$el.find('#assetName').val();
        assetInfo.owner = this.$el.find('#assetOwner').val();
        assetInfo.description = this.$el.find('#assetDescription').val();
        assetInfo.type = this.$el.find('#assetType').val();

        // For now, this dict isn't implemented due to issues with uframe's
        // acceptance of this data.
        var manufactureInfo = {};
        manufactureInfo.manufacturer = this.$el.find('#assetManufacturer').val();
        manufactureInfo.modelNumber = this.$el.find('#assetModelNumber').val();
        manufactureInfo.serialNumber = this.$el.find('#assetSerialNumber').val();

        // The metaData field is very loosly defined.  These are the only
        // field supported for asset creation at this time.
        var metaData = [
        {
            "key": "Ref Des",
            "value": this.$el.find('#assetRefDes').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Anchor Launch Date",
            "value": this.$el.find('#assetLaunchDate').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Anchor Launch Time",
            "value": this.$el.find('#assetLaunchTime').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Latitude",
            "value": this.$el.find('#assetLatitude').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Longitude",
            "value": this.$el.find('#assetLongitude').val(),
            "type": "java.lang.String"
        },
        {
            "key": "Water Depth",
            "value": this.$el.find('#assetDepth').val(),
            "type": "java.lang.String"
        }];

        // Create the new asset model that will be saved to the collection,
        // and posted to the server.
        var newAsset = new AssetModel({});
        newAsset.set('assetInfo', assetInfo);
        newAsset.set('asset_class', this.$el.find('#assetClass').val());
        newAsset.set('manufactureInfo', manufactureInfo);
        newAsset.set('notes', [ this.$el.find('#assetNotes').val() ]);
        newAsset.set('purchaseAndDeliveryInfo', this.$el.find('#assetPurchaseAndDeliveryInfo').val());
        newAsset.set('metaData', metaData);
        newAsset.set('classCode', this.$el.find('#assetClassCode').val());
        newAsset.set('seriesClassification', this.$el.find('#assetSeriesClassification').val());
        newAsset.save(null, {
            success: function(model, response){
                vent.trigger('asset:changeCollection');
            }
        });
        this.cleanUp();
        newAsset.off();
    },
    cancel: function() {
        this.cleanUp();
    },
    cleanUp: function() {
        $('#assetCreatorModal').modal('hide');
        $('body').removeClass('modal-open');
        $('.modal-backdrop').remove();
        this.remove();
        this.unbind();
    }
});
//Asset editor modal view.
var AssetEditorModalView = ParentAssetView.extend({
    /* This will render out a asset editor modal window.
     * Only certain fields are editable to date, noteably
     * the fields omitted are the reference designator.
     * - initialize()
     * - submit()
     * - cancel()
     * - destory() ... This will delete an asset...beware!
     * - cleanUp()
     */
    template: JST['ooiui/static/js/partials/AssetEditorModal.html'],
    initialize: function() {
        _.bindAll(this, 'cancel', 'submit', 'destroy', 'validateFields');
    },
    events: {
        "click button#cancelEdit" : "cancel",
        "click button#saveEdit" : "submit",
        "click button#delete" : "destroy"
    },
    validateFields: function() {

    },
    submit: function() {
        var assetInfo = this.model.get('assetInfo');
        assetInfo.name = this.$el.find('#assetName').val();
        assetInfo.owner = this.$el.find('#assetOwner').val(),
        assetInfo.description = this.$el.find('#assetDescription').val();
        assetInfo.type = this.$el.find('#assetType').val();

        this.model.set('assetId', this.model.get('id'));
        this.model.set('notes', [ this.$el.find('#assetNotes').val() ]);
        this.model.set('asset_class', this.$el.find('#assetClass').val());
        this.model.set('classCode', this.$el.find('#assetClassCode').val());
        this.model.set('assetInfo', assetInfo);
        this.model.save(null, {
            success: function(model, response){
                vent.trigger('asset:modelChange', model);
            },
            error: function(model, response){
            }
        });

        this.cleanUp();
    },
    cancel: function() {
        this.cleanUp();
    },
    destroy: function() {
        var youSure = confirm("Are you sure you would like to delete this asset?");
        if (youSure) {
            this.model.destroy();
            vent.trigger('asset:derender');
            vent.trigger('asset:changeCollection');
            this.cleanUp();
        }
    },
    cleanUp: function() {
        $('#assetEditorModal').modal('hide');
        $('body').removeClass('modal-open');
        $('.modal-backdrop').remove();
        this.derender();
    }
});
<|endoftext|>


<commit_before><commit_msg>Distinguish PC user or Mobile User #217<commit_after><|endoftext|>


<commit_before>// slacklink.js - send current active URL to slack
$( "#popup-form" ).submit(function( event ) {

  // stop form submitting
  event.preventDefault();

  // hide submit and show progress bar
  $('#submit').hide();
  $('#progress').show();

  // post link to slack
  post();

});

function post(){

	chrome.tabs.query({
		active: true,
		'lastFocusedWindow': true
	}, function(tabs) {

	    // active tab URL
	    var url = tabs[0].url;

		// grab config values from local storage

		// required variables
		var domain = localStorage.getItem('domain');
		var apitoken = localStorage.getItem('token');

		if ( !domain || !apitoken ) { // Required variables are missing so show alert

			chrome.tabs.create({ url: chrome.extension.getURL("options.html#required") });

		} else { // all required variables are set

			// optional variables (will use default values if not set)
			var channel = localStorage.getItem('channel');
			channel = (channel) ? channel : '#random';

			if ( channel.substring(0, 1) !== '#' ) {
				channel = '#' + channel; // add hash to channel if it's not there
			}

			var username = localStorage.getItem('username');
			username = (username) ? username : 'slacklink';

			var icon_emoji = localStorage.getItem('emoji');
			icon_emoji = (icon_emoji) ? icon_emoji : ':slack:';

			// do we need to add a comment to the URL
			var postcontext = $('#context').val();
			if( postcontext ){
				posttext = ( postcontext + "\n" + url );
			} else {
				posttext = url;
			}

			// payload for slack API request
			var payload = {
				channel: channel,
				username: username,
				text: posttext,
				icon_emoji: icon_emoji,
				unfurl_links: 'true'
			};

			// Insert our visual feedback dependencies
			chrome.tabs.insertCSS( null, { file: 'assets/css/visualfeedback.css' });
			chrome.tabs.executeScript( null, { file: 'assets/js/visualfeedback.js' });

			// make API request to slack
			$.ajax({

				type: 'POST',
				url: 'https://' + domain + '/services/hooks/incoming-webhook?token=' + apitoken,
				data: JSON.stringify(payload)

			}).always(function() {

				// Tidy up our visual feedback dependencies
				chrome.tabs.executeScript( null, { file: 'assets/js/tidy.js' });

				// close popup
				window.close();

			}).fail(function(){

				// Log error if post fails
				console.error('Uh oh, something\'s wrong. Check your Slacklink options');

			});
		}
	});
}<commit_msg>Added ability to send form with keyboard<commit_after>// slacklink.js - send current active URL to slack
$( "#popup-form" ).submit(function( event ) {

	// stop form submitting
	event.preventDefault();

	// hide submit and show progress bar
	$('#submit').hide();
	$('#progress').show();

	// post link to slack
	post();

});

// Submit form on Cmd + Enter or Ctrl + Enter

$('#context').keydown( function ( event ) {

	if ( ( event.ctrlKey || event.metaKey ) && event.keyCode == 13 ) {
		$('#popup-form').submit();
	}

});

function post(){

	chrome.tabs.query({
		active: true,
		'lastFocusedWindow': true
	}, function(tabs) {

		// active tab URL
		var url = tabs[0].url;

		// grab config values from local storage

		// required variables
		var domain = localStorage.getItem('domain');
		var apitoken = localStorage.getItem('token');

		if ( !domain || !apitoken ) { // Required variables are missing so show alert

			chrome.tabs.create({ url: chrome.extension.getURL("options.html#required") });

		} else { // all required variables are set

			// optional variables (will use default values if not set)
			var channel = localStorage.getItem('channel');
			channel = (channel) ? channel : '#random';

			if ( channel.substring(0, 1) !== '#' ) {
				channel = '#' + channel; // add hash to channel if it's not there
			}

			var username = localStorage.getItem('username');
			username = (username) ? username : 'slacklink';

			var icon_emoji = localStorage.getItem('emoji');
			icon_emoji = (icon_emoji) ? icon_emoji : ':slack:';

			// do we need to add a comment to the URL
			var postcontext = $('#context').val();
			if( postcontext ){
				posttext = ( postcontext + "\n" + url );
			} else {
				posttext = url;
			}

			// payload for slack API request
			var payload = {
				channel: channel,
				username: username,
				text: posttext,
				icon_emoji: icon_emoji,
				unfurl_links: 'true'
			};

			// Insert our visual feedback dependencies
			chrome.tabs.insertCSS( null, { file: 'assets/css/visualfeedback.css' });
			chrome.tabs.executeScript( null, { file: 'assets/js/visualfeedback.js' });

			// make API request to slack
			$.ajax({

				type: 'POST',
				url: 'https://' + domain + '/services/hooks/incoming-webhook?token=' + apitoken,
				data: JSON.stringify(payload)

			}).always(function() {

				// Tidy up our visual feedback dependencies
				chrome.tabs.executeScript( null, { file: 'assets/js/tidy.js' });

				// close popup
				window.close();

			}).fail(function(){

				// Log error if post fails
				console.error('Uh oh, something\'s wrong. Check your Slacklink options');

			});
		}
	});
}
<|endoftext|>