htdocs/assets/js/views/alerts/alertgroup.js

Summary

Maintainability
D
2 days
Test Coverage
"use strict";
define(function(require) {
    var _ = require('underscore'),
        View = require('view'),
        TableView = require('views/table'),
        Templates = require('templates'),
        Renderer = require('views/renderer'),
        Util = require('util'),

        Search = require('models/search'),
        Alert = require('models/alert');


    var generateQuery = function(query) {
        var parts = [];
        for(var k in query) {
            var val = query[k];
            if(_.isArray(val)) {
                val = '(' + val.join(' ') + ')';
            }
            parts.push(
                k + ':' + val
            );
        }

        return parts.join(' AND ');
    };

    var AlertFieldView = View.extend({
        template: Templates['alerts/alertentryfield'],
        tagName: 'td',
        key: null,
        value: null,
        renderedValue: null,
        rendering: false,
        hideCb: null,
        renderers: null,
        initialize: function(options) {
            this.key = options.key;
            this.value = options.value;
            this.renderers = options.renderers;
        },
        _render: function() {
            this.$el.html(this.template({
                key: this.key, value: this.value
            }));

            if(this.renderers.length > 0) {
                this.$el.popover({
                    container: this.el,
                    content: '<img src="/assets/imgs/partload.gif" />',
                    animation: false,
                    template: '<div class="renderer-data popover" role="tooltip"><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
                    html: true,
                    placement: 'bottom',
                    trigger: 'manual'
                })
                .on('shown.bs.popover', this.cbLoaded(function(e) {
                    var popover = this.$el.data('bs.popover');
                    popover.$tip.css('top', parseInt(popover.$tip.css('top'), 10) - 22 + 'px');
                }))
                .on("mouseenter", this.cbLoaded(this.show))
                .on("mouseleave", this.cbLoaded(this.hide));
            }
        },
        _unrender: function() {
            var popover = this.$el.data('bs.popover');
            if(popover) {
                popover.destroy();
            }
        },
        show: function() {
            if(this.hideCb) {
                clearTimeout(this.hideCb);
                this.hideCb = null;
                return;
            }

            this.$el.popover('show');

            // If we have rendered data, just set it. Else, pull from renderers.
            if(!_.isNull(this.renderedValue)) {
                this.update(this.renderers, this.renderedValue, false);
            } else if(!this.rendering) {
                this.rendering = true;

                var mapping = {};
                mapping[this.key] = this.renderers;
                Renderer.deserialize(this.App, mapping, 0, [this]);
            }
        },
        hide: function() {
            if(this.hideCb) {
                clearTimeout(this.hideCb);
            }
            this.hideCb = setTimeout(this.cbLoaded(function() {
                this.hideCb = null;
                this.$el.popover('hide');
            }), 200);

            return false;
        },
        update: function(renderer_list, val, preview) {
            // This view might've been destroyed by the time the callback returns. We currently don't
            // associate callbacks with views, so just explicitly check that we're still loaded here.
            if(!this.loaded()) {
                return;
            }
            if(!preview) {
                this.renderedValue = val;
                var tip = this.$el.data('bs.popover').$tip;
                tip.find('.popover-content').html(val);
            } else {
                this.$('.view').html(val);
            }
        }
    });

    var AlertEntryView = View.extend({
        tagName: 'tr',
        template: Templates['alerts/alertentry'],
        keys: null,
        full: false,
        initialize: function(options) {
            this.full = options.full;
            this.keys = options.keys;
            this.preview = options.preview;
        },
        _render: function() {
            var search = this.App.Data.Searches.get(this.model.get('search_id'));
            var vars = this.model.toJSON();
            _.extend(vars, {
                preview: this.preview,
                full: this.full,
                search_id: search ? search.get('id'):0,
                states: Alert.Data().States,
                name: search ? search.get('name'):'Unknown',
                assignee_name: Util.getAssigneeName(
                    this.model.get('assignee_type'), this.model.get('assignee'),
                    this.App.Data.Users, this.App.Data.Groups
                )
            });
            this.$el.html(this.template(vars));
            var content = Util.flatten(this.model.get('content'));

            var alert_renderers = this.model.get('renderer_data');
            var search_renderers = search ? search.get('renderer_data'):null;
            var renderers = !_.isEmpty(alert_renderers) ? alert_renderers:
                            !_.isEmpty(search_renderers) ? search_renderers:{};

            for(var i = 0; i < this.keys.length; ++i) {
                var key = this.keys[i];
                var fieldview = this.registerView(new AlertFieldView(this.App, {
                    renderers: key in renderers ? renderers[key]:[],
                    key: key,
                    value: content[key]
                }), true, this.$el, 'field[]');
            }

            var mapping = Renderer.deserialize(this.App, renderers, this.model.id, this.getView('field[]'), undefined, true);
        },
        selectAction: function(action) {
            switch(action) {
                case 'open': this.open(); break;
                case 'select': this.select(); break;
                case 'source': this.source(); break;
            }
        },
        open: function() {
            this.$('a')[0].click();
        },
        select: function() {
            this.$('input')[0].click();
        },
        source: function() {
            this.$('a')[1].click();
        },
    });

    /**
     * An alert group View
     * Dislays related alerts together in a table.
     */
    var AlertGroupView = TableView.extend({
        tagName: 'div',
        template: Templates['alerts/alertgroup'],
        subView: AlertEntryView,
        sortable: false,
        selectable: true,

        // Title of this group.
        group_name: '',
        // A list of keys to display.
        keys: null,
        // Bitmask of attributes common to the selected alerts.
        alertData: 0,
        // List of selected alerts.
        alertList: null,
        // Indicates a list from the server.
        serverList: false,
        // Whether to display ALL fields.
        full: false,
        // Index of the last entry that was clicked.
        lastEntry: 0,

        events: {
            'click .fold-button': 'toggleFold',
            'click .alerts-load-button': 'loadMoreAlerts',
            'click .alert-checkbox': 'selectRange',
            'click .alerts-select-visible': 'selectVisibleAlerts',
            'click .alerts-status': 'selectAllAlerts',
            'click .source-link': 'gotoLink',
            'click .sort-button': 'toggleSort',
            'click .download-button': 'downloadAlerts',
            'click .render-button': 'toggleRender',
        },

        initialize: function(options) {
            TableView.prototype.initialize.call(this, options);
            this.full = options.full;
            this.group_name = options.group_name;
            this.preview = options.preview;
        },
        _render: function() {
            // These attributes are initialized here because we occasionally
            // need to rerender the View.
            this.columns = [];
            this.keys = [];
            var keys = {};
            // Generate a list of all keys that these alerts contain.
            for(var j = 0; j < this.collection.length; ++j) {
                for(var k in Util.flatten(this.collection.models[j].get('content'))) {
                    keys[k] = null;
                }
            }

            for(var k in keys) {
                this.columns.push({name: k});
                this.keys.push(k);
            }

            // Determine the priority of this group.
            var search = this.App.Data.Searches.get(this.collection.query.search_id);
            var priority = search ? search.get('priority'):0;
            var levels = ['info', 'warning', 'danger'];
            this.vars = {
                name: this.group_name,
                full: this.full,
                total: this.collection.total || this.collection.length,
                state: this.collection.query.state,
                priority: priority,
                level: levels[priority],
                search_id: this.collection.query.search_id,
                escalated: this.collection.query.escalated,
                states: Alert.Data().States,
                priorities: Search.Data().Priorities,
                more: this.collection.length < this.collection.total,
                preview: this.preview,
                sortable: this.sortable,
            };

            TableView.prototype._render.call(this);
        },
        initializeCollectionData: function(params) {
            return TableView.prototype.initializeCollectionData.call(this, params);
        },
        initializeSubView: function(model) {
            var view = this.registerView(new this.subView(this.App, {
                model: model, keys: this.keys, preview: this.preview, full: this.full
            }), false, undefined, 'collection[]');
            view.load();
            return view;
        },
        update: function() {
            // Overridden update method to just render.
            // Whenever a new set of alerts come in, we rerender the entire view.
            this.initializeCollection();
            this.updateSelection();
        },
        toggleFold: function(show) {
            var elem = this.$('.table-wrapper');
            var height = elem.height();
            if(!_.isBoolean(show)) {
                show = !height;
            }

            elem.stop().animate(show ? {height: elem[0].scrollHeight}:{height: 0});

            this.$('.fold-button')
                .toggleClass('glyphicon-collapse-up', !show)
                .toggleClass('glyphicon-collapse-down', show);
        },
        toggleSort: function() {
            this.sortable = !this.sortable;
            this.rerender();
        },
        toggleRender: function() {
            this.render = !this.render;
            this.rerender();
        },
        selectRange: function(e) {
            if(e.shiftKey) {
                var checked = e.target.checked;
                var elements = this.$('.alerts-table .alert-checkbox');

                var state = 0;
                for(var i = 0; i < elements.length; ++i) {
                    if(e.target.value == elements[i].value || this.lastEntry == elements[i].value) {
                        ++state;
                    }
                    if(state === 0) {
                        continue;
                    }
                    if(state === 2) {
                        break;
                    }
                    elements[i].checked = checked;
                }
            }
            this.lastEntry = e.target.value;

            this.updateSelection();
        },
        selectVisibleAlerts: function(e) {
            var checked = e.target.checked;
            var elements = this.$('.alerts-table .alert-checkbox');

            elements.prop('checked', checked);
            this.updateSelection();
        },
        selectAllAlerts: function() {
            if(this.alertList.length == this.collection.total) {
                return;
            }

            var data = {};
            var query = _.clone(this.collection.query);
            if('from' in query) {
                data.from = query.from;
                delete query.from;
            }
            if('to' in query) {
                data.to = query.to;
                delete query.to;
            }
            data.query = '(' + this.collection.base + ') AND ' + generateQuery(query);

            this.dim();
            this.collection.getIds(data, {
                success: this.cbRendered(function(resp) {
                    this.alertList = resp;
                    this.serverList = true;
                    this.updateSelection();
                }),
                complete: this.cbRendered(this.undim),
            });
        },
        // Populate selection data.
        updateSelection: function() {
            var data = {esc: 0, assign: 0, state: 0};
            var elements = this.$('.alerts-table .alert-checkbox');
            if(elements.length === 0) {
                return;
            }

            // First, calculate the number of checks.
            var values = elements.map(function(i, x) { return x.checked; });
            var checks = 0;
            for(var i = 0; i < values.length; ++i) {
                if(values[i]) {
                    ++checks;
                }
            }
            // Update the select-all box
            this.$('.alerts-select-visible')
                .prop('indeterminate', checks > 0 && checks < values.length)
                .prop('checked', values.length == checks);

            var status_element = this.$('.alerts-status');
            var display = values.length == checks && elements.length < this.collection.total;

            if(display) {
                status_element
                    .addClass('text-info')
                    .html(
                        this.serverList ? ('All ' + this.alertList.length + ' alerts in this group are selected'):
                        (elements.length + ' alerts are selected. Click here to select all alerts in this group')
                    );
            }
            status_element.toggleClass('hidden', !display);

            // If everything is checked and the list is from the server, our range of data
            // is indeterminate.
            var user_id = this.App.Data.User.id;
            if(values.length == checks && this.serverList) {
                data.esc = 1 | 2;
                data.assign = 1 | 2 | 4;
                data.state = 1 | 2 | 4;
            } else {
                this.serverList = false;
                var form = this.$('.alerts-table > tbody');
                var form_data = Util.serializeForm(form, true, true);
                this.alertList = 'alerts' in form_data ? form_data.alerts:[];

                // Update our bitmask.
                for(var j = 0; j < this.alertList.length; ++j) {
                    var model = this.collection.get(this.alertList[j]);
                    data.esc |= 1 << model.get('escalated')|0;
                    data.state = 1 << parseInt(model.get('state'), 10);

                    var assignee_type = model.get('assignee_type');
                    var assignee = model.get('assignee');
                    var assignee_bit = assignee_type === 0 && assignee === user_id ? 2:
                        (assignee_type === 0 && assignee === 0 ? 0:1);

                    data.assign = 1 << assignee_bit;
                }
            }
            this.alertData = data;
            this.App.Bus.trigger('selection');
        },
        // Load more alerts into the table.
        loadMoreAlerts: function(e) {
            var elem = $(e.currentTarget).closest('.alerts-load').find('input').get(0);
            var n = parseInt(elem.value, 10);

            var data = {
                offset: this.collection.length,
                count: n,
            };
            var query = _.clone(this.collection.query);
            if('from' in query) {
                data.from = query.from;
                delete query.from;
            }
            if('to' in query) {
                data.to = query.to;
                delete query.to;
            }
            data.query = '(' + this.collection.base + ') AND ' + generateQuery(query);

            this.dim();
            this.collection.updateByQuery(data, {
                success: this.cbRendered(function(resp) {
                    for(var k in resp) {
                        this.collection.add(resp[k]);
                    }

                    // Rerender the alertgroup.
                    this.rerender();
                }),
                complete: this.cbRendered(this.undim),
            });
            return false;
        },
        // Return the id of all alerts that have been selected in this view.
        getSelectionData: function() {
            return [this.alertData, this.alertList];
        },
        setSelectable: function(i, selected, down) {
            if(selected) {
                this.toggleFold(true);
            }

            TableView.prototype.setSelectable.call(this, i, selected, down);
        },
        gotoLink: function(e) {
            var elem = $(e.currentTarget);
            var id = elem.data('id');
            var model = new Alert({id: id});
            model.getLink({
                async: false,
                success: this.cbRendered(function(data) {
                    if(data.link !== null) {
                        window.open(data.link, '_blank');
                    } else {
                        this.App.addMessage('No source link found');
                    }
                })
            });
        },
        downloadAlerts: function() {
            var data = JSON.stringify(this.collection.models);
            var query_str = [];
            for(var k in this.collection.query) {
                query_str.push(k + '-' + this.collection.query[k]);
            }
            Util.download(data, 'alerts_' + query_str.join('_') + '.json');
        }
    }, {
        generateQuery: generateQuery
    });

    return AlertGroupView;
});