Source: searchBar.js

/**
 * SearchBar is a Singleton. Do not call its constructor directly. Use
 * SearchBar.getInstance()
 * 
 * @classdesc
 * 
 * <p>
 * As the name suggests, SearchBar represents the search bar found in Shiviz's
 * visualization page. Both the text input and the drop-down panel are
 * considered part of the search bar. This class is responsible for binding user
 * input to the search bar with the appropriate actions.
 * </p>
 * 
 * <p>
 * Text searches, pre-defined motif searches, and user-define motif searches can
 * all be performed with the search bar. The SearchBar's mode indicates what
 * type of query is currently being performed and is one of the mode static
 * constants defined in this class (e.g. {@link SearchBar.MODE_TEXT}).
 * </p>
 * 
 * <p>
 * The search bar is associated with a {@link Global}. That global is what will
 * be searched through and modified when a search is performed.
 * </p>
 * 
 * @constructor
 */
function SearchBar() {

    if (SearchBar.instance)
        throw new Exception("Cannot instantiate SearchBar, instance already exists");

    SearchBar.instance = this;

    /** @private */
    this.global = null;

    /** @private */
    this.motifNavigator = null;

    /** @private */
    this.graphBuilder = new GraphBuilder($("#panel svg"), $("#addButton"), false);

    /** @private */
    this.mode = SearchBar.MODE_EMPTY;

    /** @private */
    this.updateLocked = false;

    var context = this;

    // Called whenever a change is made to the GraphBuilder -- either through drawing a custom structure or through clearStructure()
    this.graphBuilder.setUpdateCallback(function() {
        if (context.updateLocked) {
            return;
        }

        var vts = new VectorTimestampSerializer("{\"host\":\"`HOST`\",\"clock\":`CLOCK`}", ",", "#structure=[", "]");
        var builderGraph = this.convertToBG();
        if (!this.isCleared()) {
            context.setValue(vts.serialize(builderGraph.toVectorTimestamps()));
        }
    });

    $("#searchbar #bar input").unbind("keydown.search").on("keydown.search", function(e) {
        // Only act when panel is expanded
        if (!context.isPanelShown())
            return;

        switch (e.which) {
        // Return
        case 13:
            if (context.getValue().trim().length > 0) {
                context.addToSearchHistory(context.getValue());
                context.query();
                context.hidePanel();
            }
            break;

        // Escape
        case 27:
            context.hidePanel();
            break;
        }
        context.global.getController().bindScroll();
    });

    $("#searchbar #bar input").on("input", function() {
        context.clearResults();
        context.update();
    }).on("focus", function() {
        context.showPanel();
        context.showSearchHistory();
    });

    $("#searchButton").on("click", function(e) { 

        context.addToSearchHistory(context.getValue());

        if (e.ctrlKey && e.altKey) {
            var regexp = '(?<event>){"host":"(?<host>[^}]+)","clock":(?<clock>{[^}]*})}';
            Shiviz.getInstance().visualize(context.getValue(), regexp, "", "order", false);
        }
        else {
            context.query();
        }
        context.hidePanel();
    });

    $("#searchbar #bar .clear").on("click", function() {
        context.updateLocked = true;
        context.clear();
        context.hidePanel();
        context.update();
        context.updateLocked = false;
        context.clearMotifsTab();
        context.global.getController().bindScroll();
    });

    $("#searchbar .predefined button").on("click", function() {
        context.clearStructure();
        context.setValue("#" + this.name);
        context.addToSearchHistory("#" + this.name)
        context.hidePanel();
        context.query();
    });

    $("#nextButton").on("click", function() {
        if (context.motifNavigator == null) {
            return;
        }
        context.motifNavigator.next();
        context.hidePanel();
    });

    $("#prevButton").on("click", function() {
        if (context.motifNavigator == null) {
            return;
        }
        context.motifNavigator.prev();
        context.hidePanel();
    });

    // Event handler for switching between search options
    $("#searchbar .searchTabLinks a").on("click", function(e) {
        // Show the clicked on tab and hide the others
        var currentTab = $(this).attr("href");
        $("#searchbar #" + currentTab).show().siblings("div").hide();
        $(this).parent("li").addClass("default").siblings("li").removeClass("default");
        // prevent id of div from being added to URL
        e.preventDefault();
    });

    // Event handler for motif selection in network motifs tab
    $("#motifOption input").on("change", function() {
        if ($(this).is(":checked") || $(this).siblings("input:checkbox:checked").length > 0) {
            context.setValue("#motif");
            $("#searchButton").click();
        } else {
            context.clearText();
            $(".motifResults td").empty();
        }
        context.clearStructure();
    });
}

/**
 * @static
 * @const
 */
SearchBar.MODE_EMPTY = 0;

/**
 * @static
 * @const
 */
SearchBar.MODE_TEXT = 1;

/**
 * @static
 * @const
 */
SearchBar.MODE_CUSTOM = 2;

/**
 * @static
 * @const
 */
SearchBar.MODE_PREDEFINED = 3;

/**
 * @static
 * @const
 */
SearchBar.MODE_MOTIF = 4;

/**
 * @private
 * @static
 */
SearchBar.instance = null;

/**
 * @static
 * @const
 */
SearchBar.SEARCH_HISTORY_KEY = "searchHistory";
    
/**
 * @static
 * @const
 */
SearchBar.MAX_NUM_OF_ELEMENTS_IN_HISTORY = 64;

/**
 * Gets the SearchBar instance.
 * 
 * @static
 */
SearchBar.getInstance = function() {
    return SearchBar.instance || new SearchBar();
};

/**
 * Sets the global associated with this search bar. The global associated with
 * this search bar is what will be searched through and modified when a search
 * is performed.
 * 
 * @param {Global} global the global associated with this search bar.
 */
SearchBar.prototype.setGlobal = function(global) {
    this.global = global;
};

/**
 * Returns the global associated with this search bar.
 *
 * @returns {Global} global the global associated with this search bar
 */
SearchBar.prototype.getGlobal = function(global) {
    return this.global;
};

/**
 * Updates the mode of this search bar. The mode indicates what type of query is
 * currently being performed and is one of the mode static constants defined in
 * this class (e.g. {@link SearchBar.MODE_TEXT}). This method automatically
 * deduces what type of query is currently entered based on the contents of the
 * text field.
 */
SearchBar.prototype.updateMode = function() {
    var value = this.getValue().trim();

    $("#searchbar #bar input").css("color", "initial");

    if (value.length == 0) {
        this.mode = SearchBar.MODE_EMPTY;
        $("#searchButton").prop("disabled", true);
        $("#searchbar input").addClass("empty");
        return;
    }
    else {
        $("#searchButton").prop("disabled", false);
        $("#searchbar input").removeClass("empty");
    }

    if (value.charAt(0) != "#") {
        this.mode = SearchBar.MODE_TEXT;
    }
    else if (value.slice(0, 11) == "#structure=") {
        this.mode = SearchBar.MODE_CUSTOM;
    }
    else if (value.slice(0, 7) == "#motif") {
        this.mode = SearchBar.MODE_MOTIF;
    }
    else {
        this.mode = SearchBar.MODE_PREDEFINED;
    }

};

/**
 * Gets the current mode the search bar is in. The mode indicates what type of
 * query is currently being performed and is one of the mode static constants
 * defined in this class (e.g. {@link SearchBar.MODE_TEXT}).
 * 
 * @returns {Number} the mode
 */
SearchBar.prototype.getMode = function() {
    return this.mode;
};

/**
 * Updates the search bar to reflect any changes made to either the text or the
 * drawn graph.
 */
SearchBar.prototype.update = function() {

    this.updateLocked = true;
    this.updateMode();

    switch (this.mode) {

    // Empty
    case SearchBar.MODE_EMPTY:
        this.clearStructure();

        break;

    // Text
    case SearchBar.MODE_TEXT:
        this.clearStructure();
        break;

    // Custom structure
    case SearchBar.MODE_CUSTOM:
        try {
            var json = this.getValue().trim().match(/^#(?:structure=)?(\[.*\])/i)[1];
            var builderGraph = this.getBuilderGraphFromJSON(json);
            this.graphBuilder.convertFromBG(builderGraph);
        }
        catch (exception) {
            this.clearStructure();
            $("#searchbar #bar input").css("color", "red");
        }
        break;

    // Predefined Structure
    case SearchBar.MODE_PREDEFINED:
        this.clearStructure();
        break;

    // Network motifs
    case SearchBar.MODE_MOTIF:
        break;

    default:
        throw new Exception("Invalid mode in SearchBar");
        break;
    }

    this.updateLocked = false;
};

/**
 * Gets the value of the text in the search bar.
 * 
 * @returns {String} The text in the search bar
 */
SearchBar.prototype.getValue = function() {
    return $("#searchbar #bar input").val();
};

/**
 * Sets the value of the text in the search bar
 * 
 * @param {String} val The new value of the text in the search bar
 */
SearchBar.prototype.setValue = function(val) {
    $("#searchbar #bar input").val(val);
    this.updateMode();
};

/**
 * Determines if the drop-down panel is currently shown
 * 
 * @returns {Boolean} true if drop-down panel is shown
 */
SearchBar.prototype.isPanelShown = function() {
    return $("#searchbar #panel:visible").length;
};

/**
 * Shows the drop-down panel
 */
SearchBar.prototype.showPanel = function() {
    var context = this;

    $("#searchbar input").addClass("focus");
    $("#searchbar #panel").show();
    $(window).on("mousedown", function(e) {
        var $target = $(e.target);
        if (!$target.parents("#searchbar").length)
            context.hidePanel();
    });
};

/**
 * Shows search history in its tab
 */
 SearchBar.prototype.showSearchHistory = function() {
    var context = this;
    if (context.storageAvailable()) {
        // Remove the existing content
        var searchHistoryTab =  $("#searchbar #searchHistoryTab");
        searchHistoryTab.empty();

        // Parse the serach history and add it to the tab
        var history = [];
        history = JSON.parse(localStorage.getItem(SearchBar.SEARCH_HISTORY_KEY));
        history.reverse().forEach((previousSearch) => {
            searchHistoryTab.append('<dt class="historyItem"><code>' + previousSearch + '</code></dt>');
        })

        // Event handler for items added above
        $("#searchbar .historyItem").on("click", function(e) {
            var clickedText = jQuery(this).text();
            context.setValue(clickedText)
            context.query();
            context.hidePanel();
        });
    } 
};

/**
 * Hides the drop-down panel
 */
SearchBar.prototype.hidePanel = function() {
    $("#bar input").blur().removeClass("focus");
    $(".hostConstraintDialog").hide();
    $("#searchbar #panel").hide();
    $(window).unbind("mousedown");
};

/**
 * Clears the drawn structure
 */
SearchBar.prototype.clearStructure = function() {
    this.graphBuilder.clear();
};

/**
 * Clears the text input
 */
SearchBar.prototype.clearText = function() {
    this.setValue("");
};

/**
 * Clears search results. In other words, un-highlights found nodes and motifs
 */
SearchBar.prototype.clearResults = function() {
    $("#searchbar").removeClass("results");
    this.motifNavigator = null;
    if (this.global != null && this.global.getController().hasHighlight()) {
        this.global.getController().clearHighlight();
    } else {
        // Show the pairwise button on the log lines tab when clearing a motif search
        if (this.global.getViews().length > 1 && !$(".leftTabLinks li").first().next().hasClass("default") && !$(".pairwiseButton").is(":visible")) {
            $(".pairwiseButton").show();
        }
    }
    $(".clusterBase").removeClass("fade");
    $(".clusterResults a").removeClass("execFade");
};

/**
 * Clears the drawn motif, the text input, and the search results
 * 
 * @see {@link SearchBar#clearStructure}
 * @see {@link SearchBar#clearText}
 * @see {@link SearchBar#clearResults}
 */
SearchBar.prototype.clear = function() {
    this.clearStructure();
    this.clearText();
    this.clearResults();
};

/**
 * Performs a query based on what is currently in the text field.
 */
SearchBar.prototype.query = function() {
    this.updateMode();
    var searchbar = this;

    try {
        switch (this.mode) {
        case SearchBar.MODE_EMPTY:
            this.clearResults();
            break;

        case SearchBar.MODE_TEXT:
            var finder = new TextQueryMotifFinder(this.getValue());
            this.global.getController().highlightMotif(finder);
            break;

        case SearchBar.MODE_CUSTOM:
            try {
                var json = this.getValue().trim().match(/^#(?:structure=)?(\[.*\])/i)[1];
                var builderGraph = this.getBuilderGraphFromJSON(json);
                var finder = new CustomMotifFinder(builderGraph);
                this.global.getController().highlightMotif(finder);
            }
            catch (exception) {
                $("#searchbar #bar input").css("color", "red");
                return;
            }
            break;

        case SearchBar.MODE_PREDEFINED:
            var value = this.getValue();
            var type = value.trim().match(/^#(?:motif=)?(.*)/i)[1];

            if (type == "request-response") {
                var finder = new RequestResponseFinder(999, 4);
                this.global.getController().highlightMotif(finder);
                break;
            }
            else if (type == "broadcast" || type == "gather") {
                var broadcast;
                if (type == "broadcast")
                    broadcast = true;
                else
                    broadcast = false;

                this.global.getViews().forEach(function(view) {

                    var hiddenHosts = view.getTransformer().getHiddenHosts();
                    var hiddenHosts = view.getTransformer().getHiddenHosts();
                    var hosts = view.getHosts().filter(function(h) {
                        return !hiddenHosts[h];
                    }).length;
                    var finder = new BroadcastGatherFinder(hosts - 1, 4, broadcast);

                    view.getTransformer().highlightMotif(finder, false);
                });

                this.global.drawAll();
            }
            else {
                throw new Exception(type + " is not a built-in motif type", true);
            }

            break;

        case SearchBar.MODE_MOTIF:
            var prefix = "/shiviz/log/";
            var url = prefix + "motifs.json";

            $.get(url, function(response) {
                handleMotifResponse(response);
            }).fail(function() {
                prefix = "https://api.github.com/repos/bestchai/shiviz-logs/contents/";
                url = prefix + "motifs.json";

                $.get(url, function(response) {
                    response = atob(response.content);
                    handleMotifResponse(response);
                }).fail(function() {
                    Shiviz.getInstance().handleException(new Exception("unable to retrieve motifs from: " + url, true));
                });
            });
            break;

        default:
            throw new Exception("SearchBar.prototype.query: invalid mode");
            break;
        }
    }
    catch (e) {
        Shiviz.getInstance().handleException(e);
    }
    if (this.mode != SearchBar.MODE_MOTIF) {
        // reset the motifs tab when performing other searches
        this.clearMotifsTab();

        // For the network motifs search, motifs are only highlighted when a user clicks on an execution in the motifs tab
        // so countMotifs() should not be called during the initial search but during the on-click event in MotifDrawer.js
        $("#searchbar").addClass("results");
        this.countMotifs();
    }

    function handleMotifResponse(response) {
        var lines = response.split("\n");
        var viewToCount = {};
        var builderGraphs = [];

        // Get the relevant subgraphs from motifs.json based on ticked checkboxes
        var twoEventCutoff = lines.indexOf("2-event subgraphs");
        var threeEventCutoff = lines.indexOf("3-event subgraphs");
        var fourEventCutoff = lines.indexOf("4-event subgraphs");                

        if (!$("#motifOption #fourEvents").is(":checked")) {
            lines.splice(fourEventCutoff, lines.length - fourEventCutoff);
        }

        if (!$("#motifOption #threeEvents").is(":checked")) {
            lines.splice(threeEventCutoff, fourEventCutoff - threeEventCutoff);     
        }

        if (!$("#motifOption #twoEvents").is(":checked")) {
            var twoEventCutoff = lines.indexOf("2-event subgraphs");
            lines.splice(twoEventCutoff, threeEventCutoff - twoEventCutoff);
        }

        // Find the number of instances of a subgraph in each view
        lines.forEach(function(line) {
            if (isNaN(line.charAt(0))) {
                var builderGraph = searchbar.getBuilderGraphFromJSON(line);
                builderGraphs.push(builderGraph);

                var finder = new CustomMotifFinder(builderGraph);
                var hmt = new HighlightMotifTransformation(finder, false);

                searchbar.global.getViews().forEach(function(view) {
                    var label = view.getLabel();

                    hmt.findMotifs(view.getModel());
                    var motifGroup = hmt.getHighlighted();
                    var numMotifs = motifGroup.getMotifs().length;

                    // Save the number of instances of this motif under the current view's label
                    if (viewToCount[label]) {
                        viewToCount[label].push(numMotifs);
                    } else {
                        viewToCount[label] = [numMotifs];
                    }
                });
            }
        });
        
        // Calculate motifs and draw the results in the motifs tab
        var motifDrawer = new MotifDrawer(viewToCount, builderGraphs);
        motifDrawer.drawResults();

        // Switch to the Motifs tab and clear any previously highlighted results
        $(".leftTabLinks li").first().next().show().find("a").click();
        searchbar.clearResults();
    }
};

/**
  * Creates a BuilderGraph from a json object containing hosts and vector timestamps
  *
  * @param {String} json The json object specifying hosts and vector timestamps
  * @returns {BuilderGraph} the builderGraph created from the given json object
  */
SearchBar.prototype.getBuilderGraphFromJSON = function(json) {
    var rawRegExp = '(?<event>){"host":"(?<host>[^}]+)","clock":(?<clock>{[^}]*})}';
    var parsingRegex = new NamedRegExp(rawRegExp, "i");
    var parser = new LogParser(json, null, parsingRegex);
    var logEvents = parser.getLogEvents(parser.getLabels()[0]);
    var vectorTimestamps = logEvents.map(function(logEvent) {
        return logEvent.getVectorTimestamp();
    });
    var gbHosts = this.graphBuilder.getHosts();
    var hostConstraints = gbHosts.map(function(gbHost) {
        return gbHost.getConstraint() != "";
    });
    return BuilderGraph.fromVectorTimestamps(vectorTimestamps, hostConstraints);
}

/**
  * This function creates a new MotifNavigator to count the number of times a highlighted motif occurs in the active views
  */
SearchBar.prototype.countMotifs = function() {
    // Only compute and display the motif count if a search is being performed
    if ($("#searchbar").hasClass("results")) {
        var views = this.global.getActiveViews();
        this.motifNavigator = new MotifNavigator();
        this.motifNavigator.addMotif(views[0].getVisualModel(), views[0].getTransformer().getHighlightedMotif());
        if (this.global.getPairwiseView()) {
            this.motifNavigator.addMotif(views[1].getVisualModel(), views[1].getTransformer().getHighlightedMotif());
        }
        this.motifNavigator.start();
    
        var numMotifs = this.motifNavigator.getNumMotifs();
        var numInstances = numMotifs + " instance";
        if (numMotifs == 0 || numMotifs > 1) {
            numInstances = numInstances.concat("s");
        }
        $("#numFound").text(numInstances + " in view");
    }
};

/**
 * Clears the results in the motifs tab and uncheck all the checkboxes
 */
SearchBar.prototype.clearMotifsTab = function() {
    $("#motifOption input").prop("checked", false);
    $(".motifResults td").empty();
    $(".motifResults td:empty").remove();
}

/**
 * Resets the motif results so that no execution is selected
 */
SearchBar.prototype.resetMotifResults = function() {
    // Clear the #motif value in the searchbar if not on the motifs tab
    if (!$(".leftTabLinks li").first().next().hasClass("default")) {
        this.clearText();
    }
    $("#motifIcon").remove();
    $(".motifResults a").removeClass("indent");
}

/** 
 * Checks if local storage is supported by the browser 
 */
SearchBar.prototype.storageAvailable = function() {
    var storage;
    try {
        storage = window["localStorage"];
        var x = '__storage_test__';
        storage.setItem(x, x);
        storage.removeItem(x);
        return true;
    }
    catch(e) {
        return e instanceof DOMException && (
            // everything except Firefox
            e.code === 22 ||
            // Firefox
            e.code === 1014 ||
            // test name field too, because code might not be present
            // everything except Firefox
            e.name === 'QuotaExceededError' ||
            // Firefox
            e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
            // acknowledge QuotaExceededError only if there's something already stored
            (storage && storage.length !== 0);
    }
}

/**
 * Adds the given value to the seatch history.
 */
SearchBar.prototype.addToSearchHistory = function(item) {
    // Check if storage is available, if not, search history is not possible
    if (this.storageAvailable()) {

        // history is a queue
        var history = [];

        // Check if any item exists in the search history
        if (!localStorage.getItem(SearchBar.SEARCH_HISTORY_KEY)) {
            localStorage.setItem(SearchBar.SEARCH_HISTORY_KEY, JSON.stringify(history));
        }

        history = JSON.parse(localStorage.getItem(SearchBar.SEARCH_HISTORY_KEY));

        // Check if the value is already in the history, if true, return
        if (history.includes(item)) {
            return;
        }

        // If search history reaches limit, remove the oldest value
        if (history.length == SearchBar.MAX_NUM_OF_ELEMENTS_IN_HISTORY) {
            history.shift();
        }

        history.push(item);
        localStorage.setItem(SearchBar.SEARCH_HISTORY_KEY, JSON.stringify(history));
    }
}