<commit_before>/* global define,Jupyter,KBError */
/* global Workspace */
/* jslint white: true */
/* eslint no-console: 0 */
/**
 * @author <NAME> <<EMAIL>>
 * @public
 */
define([
    'kbwidget',
    'jquery',
    'underscore',
    'bluebird',
    'base/js/namespace',
    'narrativeConfig',
    'util/string',
    'util/display',
    'util/timeFormat',
    'util/icon',
    'kbase-client-api',
    'kbase-generic-client-api',
    'kbaseAuthenticatedWidget',
    'kbaseNarrativeDownloadPanel',
    'common/runtime',
    'handlebars',
    'text!kbase/templates/data_list/object_row.html',
    'kb_service/utils',
    'util/bootstrapAlert',

    'bootstrap',
    'jquery-nearest'
], function (
    KBWidget,
    $,
    _,
    Promise,
    Jupyter,
    Config,
    StringUtil,
    DisplayUtil,
    TimeFormat,
    Icon,
    kbase_client_api,
    GenericClient,
    kbaseAuthenticatedWidget,
    kbaseNarrativeDownloadPanel,
    Runtime,
    Handlebars,
    ObjectRowHtml,
    ServiceUtils,
    BootstrapAlert
) {
    'use strict';
    return KBWidget({
        name: 'kbaseNarrativeDataList',
        parent: kbaseAuthenticatedWidget,
        version: '1.1.0',
        options: {
            ws_name: null, // must be the WS name, not the WS Numeric ID

            ws_url: Config.url('workspace'),
            lp_url: Config.url('landing_pages'),
            loadingImage: Config.get('loading_gif'),
            parentControlPanel: null,
            slideTime: 400
        },
        // private variables
        mainListPanelHeight: '340px',
        refreshTimer: null,
        ws_name: null,
        ws: null,
        wsLastUpdateTimestamp: null,
        maxWsObjId: null,
        n_objs_rendered: 0,
        real_name_lookup: {},
        $searchInput: null,
        $filterTypeSelect: null,
        availableTypes: {},
        $searchDiv: null,
        $sortByDiv: null,
        $filterTypeDiv: null,
        $addDataButton: null,
        $controllerDiv: null,
        $mainListDiv: null,
        mainListId: null,
        $loadingDiv: null,
        objData: {}, // old style - type_name : info
        downloadSpecCache: { tag: 'dev' },
        controlClickHnd: {}, // click handlers for control buttons
        my_user_id: null,
        serviceClient: null,

        objectRowTmpl: Handlebars.compile(ObjectRowHtml),

        /** -----------------
         * Structural changes to support new view(s).
         * viewOrder = a list of object ids - which order to view (might make them references)
         * dataObjects = object id (or ref) -> object info
         * keyToObjId = key (uuid) -> obj id (int)
         */
        viewOrder: [], // { objId: int, rendered: boolean, filtered: boolean }
        dataObjects: {},
        keyToObjId: {},
        lastObjectRendered: 0,

        /* ----------------------------------------------------
            Changes for hierarchical data panel (KBASE-4566)
        */
        setItems: {}, // item_id -> {set_id -> 1, ..}
        setInfo: {}, // set_id -> { count: , div: , expanded: ,... }
        setViewMode: false, // Whether the panel is in hierarchy "mode"
        cachedSetItems: {}, // Items retrieved from a mega-call to list_sets
        dataIconParam: {},

        /*
        variables to keep track of current state before workspace refresh
        */
        selectedType : "",
        lastSortFunction : null,

        /**
         * Utility function to portably return the identifier to
         * use for a single data object.
         *
         * @param obj Info tuple from ws_list_objects
         * @return Identifier (string)
         */
        itemId: function (obj) {
            return obj[6] + '/' + obj[0];
        },

        writtingLock: false,
        refreshWrittingLock:null,

        /**
         * Test if given object is a set.
         * This simply tests whether that object is in the `setInfo` mapping.
         *
         * @param obj_info Object info tuple, as returned by ws.list_objects()
         * @return true if in a set, false otherwise
         */
        isASet: function (objInfo) {
            return this.setInfo[this.itemId(objInfo)] ? true : false;
            // return _.has(this.setInfo, this.itemId(obj_info));
        },

        /**
         * Returns true if in set view mode AND the object is a set.
         */
        isAViewedSet: function (objInfo) {
            if (this.setViewMode) {
                return this.isASet(objInfo);
            } else {
                return false;
            }
        },

        /**
         * Test if item is in a set.
         *
         * @param item_info an object info tuple, as returned by the
         *                  workspace API for list_objects()
         * @return true or false
         */
        inAnySet: function (item_info) {
            if (this.setViewMode) {
                var item_id = this.itemId(item_info);
                return _.has(this.setItems, item_id);
            } else {
                return false;
            }
        },

        getSetInfo: function (obj_info) {
            return this.setInfo[this.itemId(obj_info)];
        },

        /**
         * Get item parents.
         *
         * @param item_info an object info tuple, as returned by the
         *                  workspace API for list_objects()
         * @return All parents, expanded or not, as a mapping with
         *         the keys: (item_id, expanded, div).
         */
        getItemParents: function (item_info) {
            var item_id = this.itemId(item_info);
            // empty if not in ANY set
            if (!_.has(this.setItems, item_id)) {
                return [];
            }
            var self = this;
            // Construct return value, which is one
            // map for each of the Sets.
            return _.map(
                _.keys(this.setItems[item_id]),
                function (key) {
                    return {
                        item_id: key,
                        expanded: self.setInfo[key].expanded,
                        div: self.setInfo[key].div
                    };
                }
            );
        },

        /**
         * Clear data structures tracking the workspace sets.
         * This will cause the next refresh to fetch new data.
         */
        clearSets: function () {
            this.setItems = {};
            this.setInfo = {};
        },

        itemIdsInSet: function (set_id) {
            if (this.setViewMode) {
                return this.setInfo[set_id].item_ids;
            } else {
                return [];
            }
        },

        /**
         * @method init
         * Builds the DOM structure for the widget.
         * Includes the tables and panel.
         * If any data was passed in (options.data), that gets shoved into the datatable.
         *
         * @param {Object} - the options set.
         * @returns {Object} this shiny new widget.
         * @private
         */
        init: function (options) {
            this._super(options);
            var self = this;

            var dataConfig = Config.get('data_panel');
            // this is the limit of the number of objects to retrieve from the ws on each pass
            // note that if there are more objects than this, then sorts/search filters may
            // not show accurate results
            this.options.ws_chunk_size = dataConfig.ws_chunk_size;
            this.options.ws_max_objs_to_fetch = dataConfig.ws_max_objs_to_fetch;

            // initial number of rows to display
            this.options.objs_to_render_to_start = dataConfig.obj_render_initial;
            // number of rows to add when the user scrolls to the bottom, should be <=5,
            // much more and the addition of new rows becomes jerky
            this.options.objs_to_render_on_scroll = dataConfig.obj_render_scroll;

            //if there are more than this # of objs, user must click search
            //instead of updating as you type
            this.options.maxObjsToPreventFilterAsYouTypeInSearch = dataConfig.typeahead_search_limit;

            // initial sort makes loading slower, so we can turn it off if
            // there are more than this number of objects
            this.options.max_objs_to_prevent_initial_sort = dataConfig.initial_sort_limit;
            this.options.max_name_length = dataConfig.max_name_length;
            this.options.refresh_interval = dataConfig.refresh_interval_ms;

            this.$controllerDiv = $('<div>');
            this.$elem.append(this.$controllerDiv);
            this.renderController();

            this.loadingDiv = DisplayUtil.loadingDiv();
            this.loadingDiv.div.hide();
            this.loadingDiv.div.css({ 'top': '0', 'bottom': '0' });

            this.mainListId = StringUtil.uuid();
            this.$mainListDiv = $('<div id=' + this.mainListId + '>')
                .css({ 'overflow-x': 'hidden', 'overflow-y': 'auto', 'height': this.mainListPanelHeight })
                .on('scroll', function () {
                    if ($(this).scrollTop() + $(this).innerHeight() >= this.scrollHeight) {
                        self.renderMore();
                    }
                });

            this.$addDataButton = $('<span>').addClass('kb-data-list-add-data-button fa fa-plus fa-2x')
                .css({ 'position': 'absolute', bottom: '15px', right: '25px', 'z-index': '5' })
                .click(function () {
                    self.trigger('hideGalleryPanelOverlay.Narrative');
                    self.trigger('toggleSidePanelOverlay.Narrative', self.options.parentControlPanel.$overlayPanel);
                });
            var $mainListDivContainer = $('<div>').css({ 'position': 'relative' })
                .append(this.loadingDiv.div)
                .append(this.$mainListDiv)
                .append(this.$addDataButton.hide());
            this.$elem.append($mainListDivContainer);

            if (this._attributes.auth) {
                this.ws = new Workspace(this.options.ws_url, this._attributes.auth);
            }

            // listener for refresh
            $(document).on('updateDataList.Narrative', function () {
                self.refresh();
            });

            // self.initDataListSets();

            if (this.options.ws_name) {
                this.ws_name = this.options.ws_name;
            }

            this.wsId = Number(Jupyter.narrative.workspaceId);

            return this;
        },

        setListHeight: function (height, animate) {
            if (this.$mainListDiv) {
                if (animate) {
                    this.$mainListDiv.animate({ 'height': height }, this.options.slideTime);
                } else {
                    this.$mainListDiv.css({ 'height': height });
                }
            }
        },

        /**
         * @method loggedInCallback
         * This is associated with the login widget (through the kbaseAuthenticatedWidget parent) and
         * is triggered when a login event occurs.
         * It associates the new auth token with this widget and refreshes the data panel.
         * @private
         */
        loggedInCallback: function (event, auth) {
            this.ws = new Workspace(this.options.ws_url, auth);
            this.serviceClient = new GenericClient(Config.url('service_wizard'), auth);
            this.my_user_id = auth.user_id;
            this.isLoggedIn = true;
            this.writtingLock = false;
            this.refresh();
            return this;
        },

        /**
         * @method loggedOutCallback
         * Like the loggedInCallback, this is triggered during a logout event (through the login widget).
         * It throws away the auth token and workspace client, and refreshes the widget
         * @private
         */
        loggedOutCallback: function () {
            this.ws = null;
            this.isLoggedIn = false;
            this.my_user_id = null;
            return this;
        },

        showLoading: function (caption) {
            this.$mainListDiv.hide();
            this.loadingDiv.setText(caption || '');
            this.loadingDiv.div.show();
        },

        hideLoading: function () {
            this.loadingDiv.div.hide();
            this.$mainListDiv.show();
        },

        refresh: function (showError) {
            if(this.writtingLock) return;
            // Set the refresh timer on the first refresh. From  here, it'll refresh itself
            // every this.options.refresh_interval (30000) ms
            if (this.refreshTimer === null) {
                this.refreshTimer = setInterval(function () {
                    this.refresh();
                }.bind(this), this.options.refresh_interval); // check if there is new data every X ms
            }

            if (!this.ws_name || !this.ws) {
                console.error('DataList: missing variable(s)');
                console.error('ws_name: ' + this.ws_name);
                console.error('ws: ' + this.ws);
                return;
            }
            Promise.resolve(this.ws.get_workspace_info({
                    workspace: this.ws_name
                }))
                .then(function (wsInfo) {
                    if (this.wsLastUpdateTimestamp !== wsInfo[3]) {
                        this.wsLastUpdateTimestamp = wsInfo[3];
                        this.maxWsObjId = wsInfo[4];
                        this.showLoading('Fetching data...');
                        this.reloadWsData();
                    } else {
                        this.refreshTimeStrings();
                        // this.hideLoading();
                    }
                }.bind(this))
                .catch(function (error) {
                    console.error('DataList: when checking for updates:', error);
                    if (showError) {
                        this.showBlockingError('Sorry, an error occurred while fetching your data.', { 'error': 'Unable to connect to KBase database.' });
                    }
                }.bind(this));
        },

        refreshTimeStrings: function () {
            Object.keys(this.dataObjects).forEach(function (i) {
                if (this.dataObjects[i].$div) {
                    var newTime = TimeFormat.getTimeStampStr(this.dataObjects[i].info[3]);
                    this.dataObjects[i].$div.find('.kb-data-list-date').text(newTime);
                }
            }.bind(this));
        },

        reloadWsData: function () {
            // empty the existing object list first
            this.objData = {};
            this.availableTypes = {};

            this.viewOrder = [];
            this.dataObjects = {};

            this.clearSets();

            this.fetchWorkspaceData()
                .then(function () {
                    // Signal all data channel listeners that we have new data.
                    // TODO: only signal if there are actual changes
                    // TODO: data fetch and sychronization should live as a ui
                    // service, not in a widget.
                    var justInfo = Object.keys(this.dataObjects).map(function (objId) {
                        return this.dataObjects[objId].info;
                    }.bind(this));
                    var objectInfoPlus = Object.keys(this.dataObjects).map(function (objId) {
                        // see code below this function for the format of
                        // items in the dataObjects collection
                        var dataObject = this.dataObjects[objId];
                        var info = this.createInfoObject(dataObject.info);
                        info.dataPaletteRef = dataObject.refPath;
                        return info;
                    }.bind(this));
                    var data = JSON.parse(JSON.stringify(justInfo));
                    var runtime = Runtime.make();
                    runtime.bus().set({
                        data: data,
                        timestamp: new Date().getTime(),
                        objectInfo: objectInfoPlus
                    }, {
                        channel: 'data',
                        key: {
                            type: 'workspace-data-updated'
                        }
                    });
                }.bind(this))
                .then(function () {
                    this.showLoading('Rendering data...');
                    var numObj = Object.keys(this.dataObjects).length;
                    if (numObj > this.options.maxObjsToPreventFilterAsYouTypeInSearch) {
                        this.$searchInput.off('input');
                    }

                    if (numObj <= this.options.max_objs_to_prevent_initial_sort) {
                        this.viewOrder.sort(function (a, b) {
                            var idA = a.objId,
                                idB = b.objId;
                            if (this.dataObjects[idA].info[3] > this.dataObjects[idB].info[3]) {
                                return -1;
                            }
                            if (this.dataObjects[idA].info[3] < this.dataObjects[idB].info[3]) {
                                return 1;
                            }
                            return 0;
                        }.bind(this));
                        this.$elem.find('#nar-data-list-default-sort-option').attr('checked');
                    }

                    this.populateAvailableTypes();
                    var typeSelected = this.$filterTypeSelect.val();
                    if(this.selectedType === 'filterTypeSelect'){            
                        this.currentMatch = this.viewOrder;
                        this.filterByType(typeSelected);
                    }else if(this.selectedType === 'sortData'){
                        this.sortData(this.lastSortFunction);
                    }
                    else{
                        this.renderList();
                        this.$elem.find('#nar-data-list-default-sort-label').addClass('active');

                    }
                    this.hideLoading();
                    this.trigger('dataUpdated.Narrative');
                }.bind(this));
        },

        /**
         * @method
         * @param {string} error - the error string to show
         *
         * This empties out the main data div and injects an error into it.
         * Used mainly when lookups fail.
         */
        showBlockingError: function (title, error) {
            this.$mainListDiv.empty();
            this.$mainListDiv.append(
                DisplayUtil.createError(title, error)
            );
            this.loadingDiv.div.hide();
            this.$mainListDiv.show();
        },

        fetchWorkspaceData: function () {
            var addObjectInfo = function (objInfo, dpInfo) {
                // Get the object info
                var objId = this.itemId(objInfo); //objInfo[6] + '/' + objInfo[0]; // + '/' + objInfo[2]
                var fullDpReference = null;
                if (dpInfo && dpInfo.ref) {
                    fullDpReference = dpInfo.ref + ';' + objInfo[6] + '/' + objInfo[0] + '/' + objInfo[4];
                }
                if (this.dataObjects[objId]) {
                    return;
                }
                var key = StringUtil.uuid();
                this.dataObjects[objId] = {
                    key: key,
                    $div: null,
                    info: objInfo,
                    attached: false,
                    fromPalette: this.wsId !== objInfo[6],
                    refPath: fullDpReference
                };
                this.keyToObjId[key] = objId;
                this.viewOrder.push({
                    objId: objId,
                    inView: false,
                    inFilter: true
                });

                // set the type -> object info structure
                var typeKey = objInfo[2].split('-')[0];
                if (!(typeKey in this.objData)) {
                    this.objData[typeKey] = [];
                }
                this.objData[typeKey].push(objInfo.concat(fullDpReference));

                // get the count of objects for each type
                var typeName = typeKey.split('.')[1];
                if (!(typeName in this.availableTypes)) {
                    this.availableTypes[typeName] = {
                        type: typeName,
                        count: 0
                    }
                }
                this.availableTypes[typeName].count++;
            }.bind(this);

            var updateSetInfo = function (obj) {
                var setId = this.itemId(obj.object_info); //obj.object_info[6] + '/' + obj.object_info[0];
                obj.set_items.set_items_info.forEach(function (setItem) {
                    var itemId = this.itemId(setItem); //setItem[6] + '/' + setItem[0];
                    if (!this.setInfo[setId]) {
                        this.setInfo[setId] = {
                            div: null,
                            expanded: false,
                            item_ids: []
                        }
                    }
                    this.setInfo[setId].item_ids.push(itemId);
                    if (!this.setItems[itemId]) {
                        this.setItems[itemId] = {};
                    }
                    this.setItems[itemId][setId] = 1;
                    if (!this.dataObjects[itemId]) {
                        addObjectInfo(setItem, obj.dp_info);
                    }
                }.bind(this));
            }.bind(this);

            return Promise.resolve(
                    this.serviceClient.sync_call(
                        'NarrativeService.list_objects_with_sets', [{
                            'ws_name': this.ws_name,
                            'includeMetadata': 1
                        }]
                    )
                )
                .then(function (result) {
                    result = result[0]['data'];

                    for (var i = 0; i < result.length; i++) {
                        var obj = result[i];
                        // Skip any Narrative objects.
                        var objInfo = obj.object_info;
                        if (objInfo[2].indexOf('KBaseNarrative') === 0) {
                            if (objInfo[2].indexOf('KBaseNarrative.Narrative') === 0) {
                                Jupyter.narrative.checkDocumentVersion(objInfo);
                            }
                            continue;
                        }
                        // Only adds to dataObjects, etc., if it's not already there.
                        addObjectInfo(objInfo, obj.dp_info);
                        // if there's set info, update that.
                        if (obj.set_items) {
                            updateSetInfo(obj);
                        }
                    }
                }.bind(this))
                .catch(function (error) {
                    this.showBlockingError('Sorry, an error occurred while fetching your data.', error);
                    console.error(error);
                    KBError('kbaseNarrativeDataList.fetchWorkspaceData', error.error.message);
                    throw error;
                }.bind(this));

        },

        /**
         * Returns the available object data for a type.
         * If no type is specified (type is falsy), returns all object data
         */
        getObjData: function (type) {
            if (type) {
                var dataSet = {};
                if (typeof type === 'string') {
                    type = [type];
                }
                for (var i = 0; i < type.length; i++) {
                    if (this.objData[type[i]]) {
                        dataSet[type[i]] = this.objData[type[i]];
                    }
                }
                return dataSet;
            }
            return this.objData;
        },

        /**
         * Returns the object info for the given object.
         * if asObject is truthy, it will package this array up as an object and include the reference
         * path as key refPath.
         */
        getDataObjectByRef: function (ref, asObject) {
            if (!ref) {
                return null;
            }
            // if it's part of a ref chain, just get the last one
            if (ref.indexOf(';') >= 0) {
                var refSplit = ref.split(';');
                ref = refSplit[refSplit.length - 1];
            }
            // carve off the version, if present
            var refSegments = ref.split('/');
            if (refSegments.length < 2 || refSegments.length > 3) {
                return null;
            }
            ref = refSegments[0] + '/' + refSegments[1];
            var retVal = null;
            if (this.dataObjects[ref]) {
                retVal = this.dataObjects[ref].info;
                if (asObject) {
                    retVal = ServiceUtils.objectInfoToObject(retVal);
                    retVal.ws_id = retVal.wsid;
                    retVal.ref_path = this.dataObjects[ref].refPath;
                }
            }
            return retVal;
        },

        getDataObjectByName: function (name, wsId) {
            // means we gotta search. Oof.
            var objInfo = null;
            Object.keys(this.dataObjects).forEach(function (id) {
                var obj = this.dataObjects[id];
                if (obj.info[1] === name) {
                    if (wsId) {
                        if (wsId === obj.info[6]) {
                            objInfo = obj.info;
                        }
                    } else {
                        objInfo = obj.info;
                    }
                }
            }.bind(this));
            return objInfo;
        },

        $currentSelectedRow: null,
        selectedObject: null,

        setSelected: function ($selectedRow, object_info) {
            var self = this;
            if (self.$currentSelectedRow) {
                self.$currentSelectedRow.removeClass('kb-data-list-obj-row-selected');
            }
            if (object_info[0] === self.selectedObject) {
                self.$currentSelectedRow = null;
                self.selectedObject = null;
                self.trigger('removeFilterMethods.Narrative');
            }
        },

        addDataControls: function (object_info, $alertContainer, fromPalette, ref_path) {
            var self = this;
            var $btnToolbar = $('<span>')
                .addClass('btn-group');

            var btnClasses = 'btn btn-xs btn-default';
            var css = { 'color': '#888' };

            var $filterMethodInput = $('<span>')
                .tooltip({
                    title: 'Show Apps with this as input',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-sign-in').css(css))
                .click(function () {
                    this.trigger('filterMethods.Narrative', 'in_type:' + object_info[2].split('-')[0].split('.')[1]);
                }.bind(this));

            var $filterMethodOutput = $('<span>')
                .tooltip({
                    title: 'Show Apps with this as output',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-sign-out').css(css))
                .click(function () {
                    this.trigger('filterMethods.Narrative', 'out_type:' + object_info[2].split('-')[0].split('.')[1]);
                }.bind(this));

            var $openLandingPage = $('<span>')
                .tooltip({
                    title: 'Explore data',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-binoculars').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    // var typeTokens = object_info[2].split('-')[0].split('.');
                    var landingPageLink = self.options.lp_url + object_info[6] + '/' + object_info[1];
                    window.open(landingPageLink);
                });

            var $openHistory = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'View history to revert changes',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-history').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();

                    if (self.ws_name && self.ws) {
                        self.ws.get_object_history({ ref: object_info[6] + '/' + object_info[0] },
                            function (history) {
                                $alertContainer.append($('<div>')
                                    .append($('<button>').addClass('kb-data-list-cancel-btn')
                                        .append('Hide History')
                                        .click(function () {
                                            $alertContainer.empty();
                                        })));
                                history.reverse();
                                var $tbl = $('<table>').css({ 'width': '100%' });
                                for (var k = 0; k < history.length; k++) {
                                    var $revertBtn = $('<button>').append('v' + history[k][4]).addClass('kb-data-list-btn');
                                    if (k == 0) {
                                        $revertBtn.tooltip({
                                            title: 'Current Version',
                                            container: 'body',
                                            placement: 'bottom',
                                            delay: {
                                                show: Config.get('tooltip').showDelay,
                                                hide: Config.get('tooltip').hideDelay
                                            }
                                        });
                                    } else {
                                        var revertRef = { wsid: history[k][6], objid: history[k][0], ver: history[k][4] };
                                        (function (revertRefLocal) {
                                            $revertBtn.tooltip({
                                                    title: 'Revert to this version?',
                                                    container: 'body',
                                                    placement: 'bottom',
                                                    delay: {
                                                        show: Config.get('tooltip').showDelay,
                                                        hide: Config.get('tooltip').hideDelay
                                                    }
                                                })
                                                .click(function () {
                                                    self.ws.revert_object(revertRefLocal,
                                                        function (reverted_obj_info) {
                                                            self.writtingLock = false;
                                                            self.refresh();
                                                        },
                                                        function (error) {
                                                            console.error(error);
                                                            $alertContainer.empty();
                                                            $alertContainer.append($('<span>').css({ 'color': '#F44336' }).append('Error! ' + error.error.message));
                                                        });
                                                });
                                        })(revertRef);
                                    }
                                    $tbl.append($('<tr>')
                                        .append($('<td>').append($revertBtn))
                                        .append($('<td>').append('Saved by ' + history[k][5] + '<br>' + TimeFormat.getTimeStampStr(history[k][3])))
                                        .append($('<td>').append($('<span>').css({ margin: '4px' }).addClass('fa fa-info pull-right'))
                                            .tooltip({
                                                title: history[k][2] + '<br>' + history[k][8] + '<br>' + history[k][9] + ' bytes',
                                                container: 'body',
                                                html: true,
                                                placement: 'bottom',
                                                delay: {
                                                    show: Config.get('tooltip').showDelay,
                                                    hide: Config.get('tooltip').hideDelay
                                                }
                                            })
                                        ));
                                }
                                $alertContainer.append($tbl);
                            },
                            function (error) {
                                console.error(error);
                                $alertContainer.empty();
                                $alertContainer.append($('<span>').css({ 'color': '#F44336' }).append('Error! ' + error.error.message));
                            });
                    }


                });

            var $openProvenance = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'View data provenance and relationships',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-sitemap fa-rotate-90').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    window.open('/#objgraphview/' + object_info[7] + '/' + object_info[1]);
                });
            var $download = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'Export / Download data',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-download').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    var type = object_info[2].split('-')[0];
                    var wsId = object_info[7];
                    var objId = object_info[1];
                    var objRef = fromPalette ? ref_path : (wsId + '/' + objId);
                    var downloadPanel = $('<div>');
                    $alertContainer.append(downloadPanel);
                    new kbaseNarrativeDownloadPanel(downloadPanel, {
                        token: self._attributes.auth.token,
                        type: type,
                        objId: objId,
                        ref: objRef,
                        downloadSpecCache: self.downloadSpecCache
                    });
                });

            var $rename = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'Rename data',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-font').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    if (Jupyter.narrative.readonly) {
                        $alertContainer
                            .append($('<div>')
                                .append($('<span>')
                                    .append('Read-only Narrative - Cannot rename data object')
                                    .addClass('text-warning')));
                        return;
                    }

                    //lock on refresh expires aftger 5 min
                    var releaseLock = function(){
                        if(self.refreshWrittingLock !== null) clearTimeout(self.refreshWrittingLock);
                        
                        self.refreshWrittingLock = setTimeout(function () {
                            self.writtingLock = false;
                        }.bind(this), 900000);                        
                    };
                    var $newNameInput = $('<input type="text">')
                        .addClass('form-control')
                        .val(object_info[1])
                        .on('focus', function () {
                            if (Jupyter && Jupyter.narrative) {<commit_msg>cleanup if statements<commit_after>/* global define,Jupyter,KBError */
/* global Workspace */
/* jslint white: true */
/* eslint no-console: 0 */
/**
 * @author <NAME> <<EMAIL>>
 * @public
 */
define([
    'kbwidget',
    'jquery',
    'underscore',
    'bluebird',
    'base/js/namespace',
    'narrativeConfig',
    'util/string',
    'util/display',
    'util/timeFormat',
    'util/icon',
    'kbase-client-api',
    'kbase-generic-client-api',
    'kbaseAuthenticatedWidget',
    'kbaseNarrativeDownloadPanel',
    'common/runtime',
    'handlebars',
    'text!kbase/templates/data_list/object_row.html',
    'kb_service/utils',
    'util/bootstrapAlert',

    'bootstrap',
    'jquery-nearest'
], function (
    KBWidget,
    $,
    _,
    Promise,
    Jupyter,
    Config,
    StringUtil,
    DisplayUtil,
    TimeFormat,
    Icon,
    kbase_client_api,
    GenericClient,
    kbaseAuthenticatedWidget,
    kbaseNarrativeDownloadPanel,
    Runtime,
    Handlebars,
    ObjectRowHtml,
    ServiceUtils,
    BootstrapAlert
) {
    'use strict';
    return KBWidget({
        name: 'kbaseNarrativeDataList',
        parent: kbaseAuthenticatedWidget,
        version: '1.1.0',
        options: {
            ws_name: null, // must be the WS name, not the WS Numeric ID

            ws_url: Config.url('workspace'),
            lp_url: Config.url('landing_pages'),
            loadingImage: Config.get('loading_gif'),
            parentControlPanel: null,
            slideTime: 400
        },
        // private variables
        mainListPanelHeight: '340px',
        refreshTimer: null,
        ws_name: null,
        ws: null,
        wsLastUpdateTimestamp: null,
        maxWsObjId: null,
        n_objs_rendered: 0,
        real_name_lookup: {},
        $searchInput: null,
        $filterTypeSelect: null,
        availableTypes: {},
        $searchDiv: null,
        $sortByDiv: null,
        $filterTypeDiv: null,
        $addDataButton: null,
        $controllerDiv: null,
        $mainListDiv: null,
        mainListId: null,
        $loadingDiv: null,
        objData: {}, // old style - type_name : info
        downloadSpecCache: { tag: 'dev' },
        controlClickHnd: {}, // click handlers for control buttons
        my_user_id: null,
        serviceClient: null,

        objectRowTmpl: Handlebars.compile(ObjectRowHtml),

        /** -----------------
         * Structural changes to support new view(s).
         * viewOrder = a list of object ids - which order to view (might make them references)
         * dataObjects = object id (or ref) -> object info
         * keyToObjId = key (uuid) -> obj id (int)
         */
        viewOrder: [], // { objId: int, rendered: boolean, filtered: boolean }
        dataObjects: {},
        keyToObjId: {},
        lastObjectRendered: 0,

        /* ----------------------------------------------------
            Changes for hierarchical data panel (KBASE-4566)
        */
        setItems: {}, // item_id -> {set_id -> 1, ..}
        setInfo: {}, // set_id -> { count: , div: , expanded: ,... }
        setViewMode: false, // Whether the panel is in hierarchy "mode"
        cachedSetItems: {}, // Items retrieved from a mega-call to list_sets
        dataIconParam: {},

        /*
        variables to keep track of current state before workspace refresh
        */
        selectedType : "",
        lastSortFunction : null,

        /**
         * Utility function to portably return the identifier to
         * use for a single data object.
         *
         * @param obj Info tuple from ws_list_objects
         * @return Identifier (string)
         */
        itemId: function (obj) {
            return obj[6] + '/' + obj[0];
        },

        writtingLock: false,
        refreshWrittingLock:null,

        /**
         * Test if given object is a set.
         * This simply tests whether that object is in the `setInfo` mapping.
         *
         * @param obj_info Object info tuple, as returned by ws.list_objects()
         * @return true if in a set, false otherwise
         */
        isASet: function (objInfo) {
            return this.setInfo[this.itemId(objInfo)] ? true : false;
            // return _.has(this.setInfo, this.itemId(obj_info));
        },

        /**
         * Returns true if in set view mode AND the object is a set.
         */
        isAViewedSet: function (objInfo) {
            if (this.setViewMode) {
                return this.isASet(objInfo);
            } else {
                return false;
            }
        },

        /**
         * Test if item is in a set.
         *
         * @param item_info an object info tuple, as returned by the
         *                  workspace API for list_objects()
         * @return true or false
         */
        inAnySet: function (item_info) {
            if (this.setViewMode) {
                var item_id = this.itemId(item_info);
                return _.has(this.setItems, item_id);
            } else {
                return false;
            }
        },

        getSetInfo: function (obj_info) {
            return this.setInfo[this.itemId(obj_info)];
        },

        /**
         * Get item parents.
         *
         * @param item_info an object info tuple, as returned by the
         *                  workspace API for list_objects()
         * @return All parents, expanded or not, as a mapping with
         *         the keys: (item_id, expanded, div).
         */
        getItemParents: function (item_info) {
            var item_id = this.itemId(item_info);
            // empty if not in ANY set
            if (!_.has(this.setItems, item_id)) {
                return [];
            }
            var self = this;
            // Construct return value, which is one
            // map for each of the Sets.
            return _.map(
                _.keys(this.setItems[item_id]),
                function (key) {
                    return {
                        item_id: key,
                        expanded: self.setInfo[key].expanded,
                        div: self.setInfo[key].div
                    };
                }
            );
        },

        /**
         * Clear data structures tracking the workspace sets.
         * This will cause the next refresh to fetch new data.
         */
        clearSets: function () {
            this.setItems = {};
            this.setInfo = {};
        },

        itemIdsInSet: function (set_id) {
            if (this.setViewMode) {
                return this.setInfo[set_id].item_ids;
            } else {
                return [];
            }
        },

        /**
         * @method init
         * Builds the DOM structure for the widget.
         * Includes the tables and panel.
         * If any data was passed in (options.data), that gets shoved into the datatable.
         *
         * @param {Object} - the options set.
         * @returns {Object} this shiny new widget.
         * @private
         */
        init: function (options) {
            this._super(options);
            var self = this;

            var dataConfig = Config.get('data_panel');
            // this is the limit of the number of objects to retrieve from the ws on each pass
            // note that if there are more objects than this, then sorts/search filters may
            // not show accurate results
            this.options.ws_chunk_size = dataConfig.ws_chunk_size;
            this.options.ws_max_objs_to_fetch = dataConfig.ws_max_objs_to_fetch;

            // initial number of rows to display
            this.options.objs_to_render_to_start = dataConfig.obj_render_initial;
            // number of rows to add when the user scrolls to the bottom, should be <=5,
            // much more and the addition of new rows becomes jerky
            this.options.objs_to_render_on_scroll = dataConfig.obj_render_scroll;

            //if there are more than this # of objs, user must click search
            //instead of updating as you type
            this.options.maxObjsToPreventFilterAsYouTypeInSearch = dataConfig.typeahead_search_limit;

            // initial sort makes loading slower, so we can turn it off if
            // there are more than this number of objects
            this.options.max_objs_to_prevent_initial_sort = dataConfig.initial_sort_limit;
            this.options.max_name_length = dataConfig.max_name_length;
            this.options.refresh_interval = dataConfig.refresh_interval_ms;

            this.$controllerDiv = $('<div>');
            this.$elem.append(this.$controllerDiv);
            this.renderController();

            this.loadingDiv = DisplayUtil.loadingDiv();
            this.loadingDiv.div.hide();
            this.loadingDiv.div.css({ 'top': '0', 'bottom': '0' });

            this.mainListId = StringUtil.uuid();
            this.$mainListDiv = $('<div id=' + this.mainListId + '>')
                .css({ 'overflow-x': 'hidden', 'overflow-y': 'auto', 'height': this.mainListPanelHeight })
                .on('scroll', function () {
                    if ($(this).scrollTop() + $(this).innerHeight() >= this.scrollHeight) {
                        self.renderMore();
                    }
                });

            this.$addDataButton = $('<span>').addClass('kb-data-list-add-data-button fa fa-plus fa-2x')
                .css({ 'position': 'absolute', bottom: '15px', right: '25px', 'z-index': '5' })
                .click(function () {
                    self.trigger('hideGalleryPanelOverlay.Narrative');
                    self.trigger('toggleSidePanelOverlay.Narrative', self.options.parentControlPanel.$overlayPanel);
                });
            var $mainListDivContainer = $('<div>').css({ 'position': 'relative' })
                .append(this.loadingDiv.div)
                .append(this.$mainListDiv)
                .append(this.$addDataButton.hide());
            this.$elem.append($mainListDivContainer);

            if (this._attributes.auth) {
                this.ws = new Workspace(this.options.ws_url, this._attributes.auth);
            }

            // listener for refresh
            $(document).on('updateDataList.Narrative', function () {
                self.refresh();
            });

            // self.initDataListSets();

            if (this.options.ws_name) {
                this.ws_name = this.options.ws_name;
            }

            this.wsId = Number(Jupyter.narrative.workspaceId);

            return this;
        },

        setListHeight: function (height, animate) {
            if (this.$mainListDiv) {
                if (animate) {
                    this.$mainListDiv.animate({ 'height': height }, this.options.slideTime);
                } else {
                    this.$mainListDiv.css({ 'height': height });
                }
            }
        },

        /**
         * @method loggedInCallback
         * This is associated with the login widget (through the kbaseAuthenticatedWidget parent) and
         * is triggered when a login event occurs.
         * It associates the new auth token with this widget and refreshes the data panel.
         * @private
         */
        loggedInCallback: function (event, auth) {
            this.ws = new Workspace(this.options.ws_url, auth);
            this.serviceClient = new GenericClient(Config.url('service_wizard'), auth);
            this.my_user_id = auth.user_id;
            this.isLoggedIn = true;
            this.writtingLock = false;
            this.refresh();
            return this;
        },

        /**
         * @method loggedOutCallback
         * Like the loggedInCallback, this is triggered during a logout event (through the login widget).
         * It throws away the auth token and workspace client, and refreshes the widget
         * @private
         */
        loggedOutCallback: function () {
            this.ws = null;
            this.isLoggedIn = false;
            this.my_user_id = null;
            return this;
        },

        showLoading: function (caption) {
            this.$mainListDiv.hide();
            this.loadingDiv.setText(caption || '');
            this.loadingDiv.div.show();
        },

        hideLoading: function () {
            this.loadingDiv.div.hide();
            this.$mainListDiv.show();
        },

        refresh: function (showError) {
            if(this.writtingLock) {
                return;
            }
            // Set the refresh timer on the first refresh. From  here, it'll refresh itself
            // every this.options.refresh_interval (30000) ms
            if (this.refreshTimer === null) {
                this.refreshTimer = setInterval(function () {
                    this.refresh();
                }.bind(this), this.options.refresh_interval); // check if there is new data every X ms
            }

            if (!this.ws_name || !this.ws) {
                console.error('DataList: missing variable(s)');
                console.error('ws_name: ' + this.ws_name);
                console.error('ws: ' + this.ws);
                return;
            }
            Promise.resolve(this.ws.get_workspace_info({
                    workspace: this.ws_name
                }))
                .then(function (wsInfo) {
                    if (this.wsLastUpdateTimestamp !== wsInfo[3]) {
                        this.wsLastUpdateTimestamp = wsInfo[3];
                        this.maxWsObjId = wsInfo[4];
                        this.showLoading('Fetching data...');
                        this.reloadWsData();
                    } else {
                        this.refreshTimeStrings();
                        // this.hideLoading();
                    }
                }.bind(this))
                .catch(function (error) {
                    console.error('DataList: when checking for updates:', error);
                    if (showError) {
                        this.showBlockingError('Sorry, an error occurred while fetching your data.', { 'error': 'Unable to connect to KBase database.' });
                    }
                }.bind(this));
        },

        refreshTimeStrings: function () {
            Object.keys(this.dataObjects).forEach(function (i) {
                if (this.dataObjects[i].$div) {
                    var newTime = TimeFormat.getTimeStampStr(this.dataObjects[i].info[3]);
                    this.dataObjects[i].$div.find('.kb-data-list-date').text(newTime);
                }
            }.bind(this));
        },

        reloadWsData: function () {
            // empty the existing object list first
            this.objData = {};
            this.availableTypes = {};

            this.viewOrder = [];
            this.dataObjects = {};

            this.clearSets();

            this.fetchWorkspaceData()
                .then(function () {
                    // Signal all data channel listeners that we have new data.
                    // TODO: only signal if there are actual changes
                    // TODO: data fetch and sychronization should live as a ui
                    // service, not in a widget.
                    var justInfo = Object.keys(this.dataObjects).map(function (objId) {
                        return this.dataObjects[objId].info;
                    }.bind(this));
                    var objectInfoPlus = Object.keys(this.dataObjects).map(function (objId) {
                        // see code below this function for the format of
                        // items in the dataObjects collection
                        var dataObject = this.dataObjects[objId];
                        var info = this.createInfoObject(dataObject.info);
                        info.dataPaletteRef = dataObject.refPath;
                        return info;
                    }.bind(this));
                    var data = JSON.parse(JSON.stringify(justInfo));
                    var runtime = Runtime.make();
                    runtime.bus().set({
                        data: data,
                        timestamp: new Date().getTime(),
                        objectInfo: objectInfoPlus
                    }, {
                        channel: 'data',
                        key: {
                            type: 'workspace-data-updated'
                        }
                    });
                }.bind(this))
                .then(function () {
                    this.showLoading('Rendering data...');
                    var numObj = Object.keys(this.dataObjects).length;
                    if (numObj > this.options.maxObjsToPreventFilterAsYouTypeInSearch) {
                        this.$searchInput.off('input');
                    }

                    if (numObj <= this.options.max_objs_to_prevent_initial_sort) {
                        this.viewOrder.sort(function (a, b) {
                            var idA = a.objId,
                                idB = b.objId;
                            if (this.dataObjects[idA].info[3] > this.dataObjects[idB].info[3]) {
                                return -1;
                            }
                            if (this.dataObjects[idA].info[3] < this.dataObjects[idB].info[3]) {
                                return 1;
                            }
                            return 0;
                        }.bind(this));
                        this.$elem.find('#nar-data-list-default-sort-option').attr('checked');
                    }

                    this.populateAvailableTypes();
                    var typeSelected = this.$filterTypeSelect.val();
                    if(this.selectedType === 'filterTypeSelect'){            
                        this.currentMatch = this.viewOrder;
                        this.filterByType(typeSelected);
                    }else if(this.selectedType === 'sortData'){
                        this.sortData(this.lastSortFunction);
                    }
                    else{
                        this.renderList();
                        this.$elem.find('#nar-data-list-default-sort-label').addClass('active');

                    }
                    this.hideLoading();
                    this.trigger('dataUpdated.Narrative');
                }.bind(this));
        },

        /**
         * @method
         * @param {string} error - the error string to show
         *
         * This empties out the main data div and injects an error into it.
         * Used mainly when lookups fail.
         */
        showBlockingError: function (title, error) {
            this.$mainListDiv.empty();
            this.$mainListDiv.append(
                DisplayUtil.createError(title, error)
            );
            this.loadingDiv.div.hide();
            this.$mainListDiv.show();
        },

        fetchWorkspaceData: function () {
            var addObjectInfo = function (objInfo, dpInfo) {
                // Get the object info
                var objId = this.itemId(objInfo); //objInfo[6] + '/' + objInfo[0]; // + '/' + objInfo[2]
                var fullDpReference = null;
                if (dpInfo && dpInfo.ref) {
                    fullDpReference = dpInfo.ref + ';' + objInfo[6] + '/' + objInfo[0] + '/' + objInfo[4];
                }
                if (this.dataObjects[objId]) {
                    return;
                }
                var key = StringUtil.uuid();
                this.dataObjects[objId] = {
                    key: key,
                    $div: null,
                    info: objInfo,
                    attached: false,
                    fromPalette: this.wsId !== objInfo[6],
                    refPath: fullDpReference
                };
                this.keyToObjId[key] = objId;
                this.viewOrder.push({
                    objId: objId,
                    inView: false,
                    inFilter: true
                });

                // set the type -> object info structure
                var typeKey = objInfo[2].split('-')[0];
                if (!(typeKey in this.objData)) {
                    this.objData[typeKey] = [];
                }
                this.objData[typeKey].push(objInfo.concat(fullDpReference));

                // get the count of objects for each type
                var typeName = typeKey.split('.')[1];
                if (!(typeName in this.availableTypes)) {
                    this.availableTypes[typeName] = {
                        type: typeName,
                        count: 0
                    }
                }
                this.availableTypes[typeName].count++;
            }.bind(this);

            var updateSetInfo = function (obj) {
                var setId = this.itemId(obj.object_info); //obj.object_info[6] + '/' + obj.object_info[0];
                obj.set_items.set_items_info.forEach(function (setItem) {
                    var itemId = this.itemId(setItem); //setItem[6] + '/' + setItem[0];
                    if (!this.setInfo[setId]) {
                        this.setInfo[setId] = {
                            div: null,
                            expanded: false,
                            item_ids: []
                        }
                    }
                    this.setInfo[setId].item_ids.push(itemId);
                    if (!this.setItems[itemId]) {
                        this.setItems[itemId] = {};
                    }
                    this.setItems[itemId][setId] = 1;
                    if (!this.dataObjects[itemId]) {
                        addObjectInfo(setItem, obj.dp_info);
                    }
                }.bind(this));
            }.bind(this);

            return Promise.resolve(
                    this.serviceClient.sync_call(
                        'NarrativeService.list_objects_with_sets', [{
                            'ws_name': this.ws_name,
                            'includeMetadata': 1
                        }]
                    )
                )
                .then(function (result) {
                    result = result[0]['data'];

                    for (var i = 0; i < result.length; i++) {
                        var obj = result[i];
                        // Skip any Narrative objects.
                        var objInfo = obj.object_info;
                        if (objInfo[2].indexOf('KBaseNarrative') === 0) {
                            if (objInfo[2].indexOf('KBaseNarrative.Narrative') === 0) {
                                Jupyter.narrative.checkDocumentVersion(objInfo);
                            }
                            continue;
                        }
                        // Only adds to dataObjects, etc., if it's not already there.
                        addObjectInfo(objInfo, obj.dp_info);
                        // if there's set info, update that.
                        if (obj.set_items) {
                            updateSetInfo(obj);
                        }
                    }
                }.bind(this))
                .catch(function (error) {
                    this.showBlockingError('Sorry, an error occurred while fetching your data.', error);
                    console.error(error);
                    KBError('kbaseNarrativeDataList.fetchWorkspaceData', error.error.message);
                    throw error;
                }.bind(this));

        },

        /**
         * Returns the available object data for a type.
         * If no type is specified (type is falsy), returns all object data
         */
        getObjData: function (type) {
            if (type) {
                var dataSet = {};
                if (typeof type === 'string') {
                    type = [type];
                }
                for (var i = 0; i < type.length; i++) {
                    if (this.objData[type[i]]) {
                        dataSet[type[i]] = this.objData[type[i]];
                    }
                }
                return dataSet;
            }
            return this.objData;
        },

        /**
         * Returns the object info for the given object.
         * if asObject is truthy, it will package this array up as an object and include the reference
         * path as key refPath.
         */
        getDataObjectByRef: function (ref, asObject) {
            if (!ref) {
                return null;
            }
            // if it's part of a ref chain, just get the last one
            if (ref.indexOf(';') >= 0) {
                var refSplit = ref.split(';');
                ref = refSplit[refSplit.length - 1];
            }
            // carve off the version, if present
            var refSegments = ref.split('/');
            if (refSegments.length < 2 || refSegments.length > 3) {
                return null;
            }
            ref = refSegments[0] + '/' + refSegments[1];
            var retVal = null;
            if (this.dataObjects[ref]) {
                retVal = this.dataObjects[ref].info;
                if (asObject) {
                    retVal = ServiceUtils.objectInfoToObject(retVal);
                    retVal.ws_id = retVal.wsid;
                    retVal.ref_path = this.dataObjects[ref].refPath;
                }
            }
            return retVal;
        },

        getDataObjectByName: function (name, wsId) {
            // means we gotta search. Oof.
            var objInfo = null;
            Object.keys(this.dataObjects).forEach(function (id) {
                var obj = this.dataObjects[id];
                if (obj.info[1] === name) {
                    if (wsId) {
                        if (wsId === obj.info[6]) {
                            objInfo = obj.info;
                        }
                    } else {
                        objInfo = obj.info;
                    }
                }
            }.bind(this));
            return objInfo;
        },

        $currentSelectedRow: null,
        selectedObject: null,

        setSelected: function ($selectedRow, object_info) {
            var self = this;
            if (self.$currentSelectedRow) {
                self.$currentSelectedRow.removeClass('kb-data-list-obj-row-selected');
            }
            if (object_info[0] === self.selectedObject) {
                self.$currentSelectedRow = null;
                self.selectedObject = null;
                self.trigger('removeFilterMethods.Narrative');
            }
        },

        addDataControls: function (object_info, $alertContainer, fromPalette, ref_path) {
            var self = this;
            var $btnToolbar = $('<span>')
                .addClass('btn-group');

            var btnClasses = 'btn btn-xs btn-default';
            var css = { 'color': '#888' };

            var $filterMethodInput = $('<span>')
                .tooltip({
                    title: 'Show Apps with this as input',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-sign-in').css(css))
                .click(function () {
                    this.trigger('filterMethods.Narrative', 'in_type:' + object_info[2].split('-')[0].split('.')[1]);
                }.bind(this));

            var $filterMethodOutput = $('<span>')
                .tooltip({
                    title: 'Show Apps with this as output',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-sign-out').css(css))
                .click(function () {
                    this.trigger('filterMethods.Narrative', 'out_type:' + object_info[2].split('-')[0].split('.')[1]);
                }.bind(this));

            var $openLandingPage = $('<span>')
                .tooltip({
                    title: 'Explore data',
                    container: '#' + this.mainListId,
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .addClass(btnClasses)
                .append($('<span>').addClass('fa fa-binoculars').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    // var typeTokens = object_info[2].split('-')[0].split('.');
                    var landingPageLink = self.options.lp_url + object_info[6] + '/' + object_info[1];
                    window.open(landingPageLink);
                });

            var $openHistory = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'View history to revert changes',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-history').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();

                    if (self.ws_name && self.ws) {
                        self.ws.get_object_history({ ref: object_info[6] + '/' + object_info[0] },
                            function (history) {
                                $alertContainer.append($('<div>')
                                    .append($('<button>').addClass('kb-data-list-cancel-btn')
                                        .append('Hide History')
                                        .click(function () {
                                            $alertContainer.empty();
                                        })));
                                history.reverse();
                                var $tbl = $('<table>').css({ 'width': '100%' });
                                for (var k = 0; k < history.length; k++) {
                                    var $revertBtn = $('<button>').append('v' + history[k][4]).addClass('kb-data-list-btn');
                                    if (k == 0) {
                                        $revertBtn.tooltip({
                                            title: 'Current Version',
                                            container: 'body',
                                            placement: 'bottom',
                                            delay: {
                                                show: Config.get('tooltip').showDelay,
                                                hide: Config.get('tooltip').hideDelay
                                            }
                                        });
                                    } else {
                                        var revertRef = { wsid: history[k][6], objid: history[k][0], ver: history[k][4] };
                                        (function (revertRefLocal) {
                                            $revertBtn.tooltip({
                                                    title: 'Revert to this version?',
                                                    container: 'body',
                                                    placement: 'bottom',
                                                    delay: {
                                                        show: Config.get('tooltip').showDelay,
                                                        hide: Config.get('tooltip').hideDelay
                                                    }
                                                })
                                                .click(function () {
                                                    self.ws.revert_object(revertRefLocal,
                                                        function (reverted_obj_info) {
                                                            self.writtingLock = false;
                                                            self.refresh();
                                                        },
                                                        function (error) {
                                                            console.error(error);
                                                            $alertContainer.empty();
                                                            $alertContainer.append($('<span>').css({ 'color': '#F44336' }).append('Error! ' + error.error.message));
                                                        });
                                                });
                                        })(revertRef);
                                    }
                                    $tbl.append($('<tr>')
                                        .append($('<td>').append($revertBtn))
                                        .append($('<td>').append('Saved by ' + history[k][5] + '<br>' + TimeFormat.getTimeStampStr(history[k][3])))
                                        .append($('<td>').append($('<span>').css({ margin: '4px' }).addClass('fa fa-info pull-right'))
                                            .tooltip({
                                                title: history[k][2] + '<br>' + history[k][8] + '<br>' + history[k][9] + ' bytes',
                                                container: 'body',
                                                html: true,
                                                placement: 'bottom',
                                                delay: {
                                                    show: Config.get('tooltip').showDelay,
                                                    hide: Config.get('tooltip').hideDelay
                                                }
                                            })
                                        ));
                                }
                                $alertContainer.append($tbl);
                            },
                            function (error) {
                                console.error(error);
                                $alertContainer.empty();
                                $alertContainer.append($('<span>').css({ 'color': '#F44336' }).append('Error! ' + error.error.message));
                            });
                    }


                });

            var $openProvenance = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'View data provenance and relationships',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-sitemap fa-rotate-90').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    window.open('/#objgraphview/' + object_info[7] + '/' + object_info[1]);
                });
            var $download = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'Export / Download data',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-download').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    var type = object_info[2].split('-')[0];
                    var wsId = object_info[7];
                    var objId = object_info[1];
                    var objRef = fromPalette ? ref_path : (wsId + '/' + objId);
                    var downloadPanel = $('<div>');
                    $alertContainer.append(downloadPanel);
                    new kbaseNarrativeDownloadPanel(downloadPanel, {
                        token: self._attributes.auth.token,
                        type: type,
                        objId: objId,
                        ref: objRef,
                        downloadSpecCache: self.downloadSpecCache
                    });
                });

            var $rename = $('<span>')
                .addClass(btnClasses).css(css)
                .tooltip({
                    title: 'Rename data',
                    container: 'body',
                    delay: {
                        show: Config.get('tooltip').showDelay,
                        hide: Config.get('tooltip').hideDelay
                    }
                })
                .append($('<span>').addClass('fa fa-font').css(css))
                .click(function (e) {
                    e.stopPropagation();
                    $alertContainer.empty();
                    if (Jupyter.narrative.readonly) {
                        $alertContainer
                            .append($('<div>')
                                .append($('<span>')
                                    .append('Read-only Narrative - Cannot rename data object')
                                    .addClass('text-warning')));
                        return;
                    }

                    //lock on refresh expires after 15 min
                    var releaseLock = function(){
                        if(self.refreshWrittingLock !== null) {
                            clearTimeout(self.refreshWrittingLock);
                        }
                        
                        self.refreshWrittingLock = setTimeout(function () {
                            self.writtingLock = false;
                        }, 900000);                        
                    };
                    var $newNameInput = $('<input type="text">')
                        .addClass('form-control')
                        .val(object_info[1])
                        .on('focus', function () {
                            if (Jupyter && Jupyter.narrative) {
{'fraction_non_alphanumeric': 0.11146881287726358, 'fraction_numerical': 0.003465957032517687, 'mean_word_length': 1.0945078847199565, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 105, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 234, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4630485', 'n_tokens_mistral': 18872, 'n_tokens_neox': 17751, 'n_words': 3937}