<commit_before>/**
 * @author <NAME> <<EMAIL>>
 * @public
 */
(function( $, undefined ) {
    $.KBWidget({
        name: 'kbaseNarrativeDataList',
        parent: 'kbaseAuthenticatedWidget',
        version: '1.0.0',
        options: {
            ws_name: null, // must be the WS name, not the WS Numeric ID

            ws_url:"https://kbase.us/services/ws",
            landing_page_url: "/functional-site/#/", // !! always include trailing slash
            default_landing_page_url: "/functional-site/#/ws/json/", // ws_name/obj_name,
            
            user_name_fetch_url:"https://kbase.us/services/genome_comparison/users?usernames=",

            loadingImage: 'static/kbase/images/ajax-loader.gif',
            methodStoreURL: 'http://dev19.berkeley.kbase.us/narrative_method_store',

            ws_chunk_size:10000,  // this is the limit of the number of objects to retrieve from the ws on each pass
            ws_max_objs_to_fetch: 75000, // this is the total limit of the number of objects before we stop trying to get more
                                         // note that if there are more objects than this, then sorts/search filters may
                                         // not show accurate results

            objs_to_render_to_start:40, // initial number of rows to display
            objs_to_render_on_scroll:5, // 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

            max_objs_to_prevent_filter_as_you_type_in_search:50000, //if there are more than this # of objs, user must click search
                                                                    //instead of updating as you type

            max_objs_to_prevent_initial_sort:10000, // initial sort makes loading slower, so we can turn it off if
                                                    // there are more than this number of objects

            max_name_length:33,
            refresh_interval:30000,

            parentControlPanel: null
        },

        // private variables
        mainListPanelHeight : '340px',

        ws_name: null,
        ws: null,
        ws_last_update_timestamp: null,
        ws_obj_count: null,

        n_objs_rendered:0,

        ws_landing_page_map: {},
        real_name_lookup: {},

        $searchInput: null,
        $filterTypeSelect: null,
        availableTypes:{},

        $searchDiv: null,
        $sortByDiv: null,
        $filterTypeDiv: null,

        $addDataButton:null,
        $controllerDiv: null,
        $mainListDiv:null,
        mainListId:null,
        $loadingDiv:null,

        methClient: null,

        obj_list : [],
        obj_data : {}, // old style - type_name : info


    
        /**
         * @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;
            this.getLandingPageMap();  //start off this request so that we hopefully get something back right away

            this.$controllerDiv = $('<div>');
            this.$elem.append(this.$controllerDiv);
            this.renderController();
            this.$loadingDiv = $('<div>').addClass('kb-data-loading')
                                 .append('<img src="' + this.options.loadingImage + '">');
            this.$elem.append(this.$loadingDiv);
            this.mainListId=this.genUUID();
            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.$mainListDiv)
                                            .append(this.$addDataButton.hide());
            this.$elem.append($mainListDivContainer);

            if (window.kbconfig === undefined || window.kbconfig.urls === undefined ||
                window.kbconfig.data_icons === undefined) {
              // bail out now
              alert("Failed to load base configuration! Aborting narrative now.");
              window.location = "/"; //XXX: Need to load the error page!!
            }
            this.options.methodStoreURL = window.kbconfig.urls.narrative_method_store;
            this.options.ws_url = window.kbconfig.urls.workspace;
            this.data_icons = window.kbconfig.data_icons;

            if (this._attributes.auth) {
                this.ws = new Workspace(this.options.ws_url, this._attributes.auth);
            }
            setInterval(function(){self.refresh();}, this.options.refresh_interval); // check if there is new data every X ms

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

            this.showLoading();
            if (this.options.ws_name) {
                this.setWorkspace(this.options.ws_name);
            }

            this.methClient = new NarrativeMethodStore(this.options.methodStoreURL);

            return this;
        },

        setWorkspace : function(ws_name) {
            this.ws_name = ws_name;
            //this.ws_name = "janakacore"; // for testing a bigish workspace
            //this.ws_name = "KBasePublicGenomesV4"; // for testing a very big workspace
            this.refresh();
        },

        refresh: function() {
            var self = this;
            if (self.ws_name && self.ws) {
                self.ws.get_workspace_info({
                        workspace: this.ws_name
                    },
                    function(workspace_info) {
                        //[0] ws_id id, [1] ws_name workspace, [2] username owner, [3] timestamp moddate,
                        //[4] int object, [5] permission user_permission, [6] permission globalread,
                        //[7] lock_status lockstat, [8] usermeta metadata
                        //console.log('I have: '+self.ws_last_update_timestamp+ " remote has: "+workspace_info[3]);
                        if (self.ws_last_update_timestamp) {
                            if (self.ws_last_update_timestamp !== workspace_info[3]) {
                                self.ws_last_update_timestamp = workspace_info[3];
                                self.ws_obj_count = workspace_info[4];
                                self.reloadWsData();
                            } else {
                                //console.log('updating times');
                                self.refreshTimeStrings();
                            }
                        } else {
                            self.ws_last_update_timestamp = workspace_info[3];
                            self.ws_obj_count = workspace_info[4];
                            self.reloadWsData();
                        }
                    },
                    function(error) {
                        // don't worry about it, just do nothing...
                        self.$mainListDiv.show();
                        self.$mainListDiv.append("error: ");
                        self.$mainListDiv.append(error.error.message);
                        console.error(error);
                        self.hideLoading();
                    });
            } // else { we should probably do something if the user is not logged in or if the ws isn't set yet }
        },
        
        refreshSpecificObject: function() {
            
        },

        refreshTimeStrings: function() {
            var self = this; var newTime; var oldTime;
            if (self.objectList) {
                for(var i=0; i<self.objectList.length; i++) {
                    if(self.objectList[i].$div) {
                        newTime = self.getTimeStampStr(self.objectList[i].info[3]);
                        oldTime = self.objectList[i].$div.find('.kb-data-list-date').text();
                        if (newTime !== oldTime) {
                            self.objectList[i].$div.find('.kb-data-list-date').text(newTime);
                        }
                    }
                }
            }
        },

        reloadWsData: function () {
            var self = this;
            if (self.ws_name && self.ws) {
                // empty the existing object list first
                self.objectList = [];
                self.obj_data = {};
                self.availableTypes = {};

                self.getNextDataChunk(0);
            }
        },

        getNextDataChunk: function(skip) {
            var self = this;
            self.ws.list_objects({
                    workspaces : [self.ws_name],
                    includeMetadata: 1,
                    skip: skip,
                    limit: self.options.ws_chunk_size
                },
                function(infoList) {
                    // object_info:
                    // [0] : obj_id objid // [1] : obj_name name // [2] : type_string type
                    // [3] : timestamp save_date // [4] : int version // [5] : username saved_by
                    // [6] : ws_id wsid // [7] : ws_name workspace // [8] : string chsum
                    // [9] : int size // [10] : usermeta meta
                    for (var i=0; i<infoList.length; i++) {
                        // skip narrative objects
                        if (infoList[i][2].indexOf('KBaseNarrative') == 0) { continue; }
                        self.objectList.push(
                            {
                                $div:null, //self.renderObjectRowDiv(infoList[i]), // we defer rendering the div until it is shown
                                info:infoList[i],
                                attached:false
                            }
                        );
                        var typeKey = infoList[i][2].split("-")[0];
                        if (!(typeKey in self.obj_data)) {
                            self.obj_data[typeKey]=[];
                        }
                        self.obj_data[typeKey].push(infoList[i]);

                        var typeName = typeKey.split('.')[1];
                        if (!(typeName in self.availableTypes)) {
                            self.availableTypes[typeName] =
                                        {
                                            type:typeName,
                                            count:0
                                        };
                        }
                        self.availableTypes[typeName].count++;
                    }

                    // if we have more than 2k objects, make them hit enter to search...
                    self.$searchInput.off("input change blur");
                    self.$searchInput.on("change blur",function() { self.search(); });
                    if (self.objectList.length<=self.options.max_objs_to_prevent_filter_as_you_type_in_search) {
                        self.$searchInput.on("input",function() { self.search(); });
                    }

                    self.trigger('dataUpdated.Narrative');

                    //LOGIC: we keep trying to get more until we reach the ws_obj_count or untill the max
                    // fetch count option, UNLESS the last call returned nothing, in which case we stop.
                    //IMPORTANT NOTE: IN RARE CASES THIS DOES NOT GAURANTEE THAT WE GET ALL OBJECTS FROM
                    //THIS WS!!  IF THERE IS A CHUNK THAT RETURNED NOTHING, THERE STILL MAY BE MORE
                    //OBJECTS DUE TO A BUG IN THE WORKSPACE THAT INCLUDES OLD VERSIONS AND DELETED VERSIONS
                    //BEFORE FILTERING OUT THE NUMBER - A BETTER TEMP FIX WOULD BE TO LIMIT THE NUMBER OF
                    //RECURSIONS TO 2 or 3 MAYBE...
                    if (self.objectList.length < self.ws_obj_count
                            && self.objectList.length < self.options.ws_max_objs_to_fetch
                            && infoList.length>0) {
                        self.getNextDataChunk(skip+self.options.ws_chunk_size);
                    } else {
                        if (self.objectList.length<=self.options.max_objs_to_prevent_initial_sort) {
                            self.objectList.sort(function(a,b) {
                                    if (a.info[3] > b.info[3]) return -1; // sort by date
                                    if (a.info[3] < b.info[3]) return 1;  // sort by date
                                    return 0;
                                });
                            self.$elem.find('#nar-data-list-default-sort-label').addClass('active');
                            self.$elem.find('#nar-data-list-default-sort-option').attr('checked');
                        }
                    }

                    self.populateAvailableTypes();
                    self.renderList();
                    self.hideLoading();
                },
                function(error) {
                    self.$mainListDiv.show();
                    self.$mainListDiv.append("error: ");
                    self.$mainListDiv.append(error.error.message);
                    console.error(error);
                    self.hideLoading();
                });

        },

        getObjData: function(type, ignoreVersion) {
            if (type) {
                var dataSet = {};
                if (typeof type === 'string') {
                    type = [type];
                }
                for (var i=0; i<type.length; i++) {
                    if (this.obj_data[type[i]]) {
                        dataSet[type[i]]=this.obj_data[type[i]];
                    }
                }
                return dataSet;
            }
            return this.obj_data;
        },

        $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');
            } else {
                $selectedRow.addClass('kb-data-list-obj-row-selected');
                self.$currentSelectedRow = $selectedRow;
                self.selectedObject = object_info[0];
                self.trigger('filterMethods.Narrative','type:'+object_info[2].split('-')[0].split('.')[1]);
            }
        },
        
        
        addDataControls: function(object_info, $alertContainer) {
            var self = this;
            var $btnToolbar = $('<span>')
                                        .addClass('btn-toolbar')
                                        .attr('role', 'toolbar');
            
            var btnClasses = "btn btn-xs btn-default";
            var css = {'color':'#888'};
            
                                /*.append($('<div>').css({'text-align':'center','margin':'5pt'})
                                            .append('<a href="'+landingPageLink+'" target="_blank">'+
                                                        'explore data</a>&nbsp&nbsp|&nbsp&nbsp')
                                            .append('<a href="'+this.options.landing_page_url+'objgraphview/'+object_info[7] +'/'+object_info[1] +'" target="_blank">'+
                                                        'view provenance</a><br>'))*/
            
            var $openLandingPage = $('<span>')
                                        .tooltip({title:'Explore data', 'container':'#'+this.mainListId})
                                        .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.default_landing_page_url +object_info[7]+ '/' + object_info[1];
                                            if (self.ws_landing_page_map) {
                                                if (self.ws_landing_page_map[typeTokens[0]]) {
                                                    if (self.ws_landing_page_map[typeTokens[0]][typeTokens[1]]) {
                                                        landingPageLink = self.options.landing_page_url +
                                                            self.ws_landing_page_map[typeTokens[0]][typeTokens[1]] + "/" +
                                                            object_info[7]+ '/' + object_info[1];
                                                    }
                                                }
                                            }
                                            window.open(landingPageLink);
                                        });
                                        
            var $openHistory = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'View history to revert changes', 'container':'body'})
                                        .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) {
                                                        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'});
                                                            } else {
                                                                var revertRef = {wsid:history[k][6], objid:history[k][0], ver:history[k][4]};
                                                                $revertBtn.tooltip({title:'Revert to this version?', 'container':'body',placement:'bottom'})
                                                                    .click(function() {
                                                                        self.ws.revert_object(revertRef,
                                                                            function(reverted_obj_info) {
                                                                                self.refresh();
                                                                            }, function(error) {
                                                                                console.error(error);
                                                                                $alertContainer.empty();
                                                                                $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                            });
                                                                    })
                                                            }
                                                            $tbl.append($('<tr>')
                                                                        .append($('<td>').append($revertBtn))
                                                                        .append($('<td>').append('Saved by '+history[k][5]+'<br>'+self.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'}))
                                                                                );
                                                        }
                                                        $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'})
                                        .append($('<span>').addClass('fa fa-sitemap fa-rotate-90').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            window.open(self.options.landing_page_url+'objgraphview/'+object_info[7]+'/'+object_info[1]);
                                        });
            var $download = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Export / Download data', 'container':'body'})
                                        .append($('<span>').addClass('fa fa-download').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            $alertContainer.append('Coming soon');
                                        });
            
            var $rename = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Rename data', 'container':'body'})
                                        .append($('<span>').addClass('fa fa-font').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            var $newNameInput = $('<input type="text">').addClass('form-control').val(object_info[1]);
                                            $alertContainer.append($('<div>')
                                                .append($('<div>').append("Warning: Apps using the old name may break."))
                                                .append($('<div>').append($newNameInput))
                                                .append($('<button>').addClass('kb-data-list-btn')
                                                            .append('Rename')
                                                            .click(function() {
                                                                if (self.ws_name && self.ws) {
                                                                    self.ws.rename_object({
                                                                            obj: {ref:object_info[6]+"/"+object_info[0]},
                                                                            new_name: $newNameInput.val()
                                                                        },
                                                                        function(renamed_info) {
                                                                            self.refresh();
                                                                        },
                                                                        function(error) {
                                                                            console.error(error);
                                                                            $alertContainer.empty();
                                                                            $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                        });
                                                                }
                                                            }))
                                                .append($('<button>').addClass('kb-data-list-cancel-btn')
                                                            .append('Cancel')
                                                            .click(function() {$alertContainer.empty();} )));
                                        });
            var $delete = $('<span>')   
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Delete data'})
                                        .append($('<span>').addClass('fa fa-trash-o').css(css))
                                        .click(function(e) {
                                            e.stopPropagation();
                                            $alertContainer.empty();
                                            $alertContainer.append($('<div>')
                                                .append($('<span>').append('Are you sure?'))
                                                .append($('<button>').addClass('kb-data-list-btn')
                                                            .append('Delete')
                                                            .click(function() {
                                                                if (self.ws_name && self.ws) {
                                                                    self.ws.rename_object({
                                                                            obj: {ref:object_info[6]+"/"+object_info[0]},
                                                                            new_name: object_info[1].split('-deleted-')[0] + "-deleted-"+(new Date()).getTime()
                                                                        },
                                                                        function(renamed_info) {
                                                                            self.ws.delete_objects([{ref:object_info[6]+"/"+object_info[0]}],
                                                                                function() {
                                                                                    self.refresh();
                                                                                },
                                                                                function(error) {
                                                                                    console.error(error);
                                                                                    $alertContainer.empty();
                                                                                    $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                                });
                                                                        },
                                                                        function(error) {
                                                                            console.error(error);
                                                                            $alertContainer.empty();
                                                                            $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                        });
                                                                }
                                                            }))
                                                .append($('<button>').addClass('kb-data-list-cancel-btn')
                                                            .append('Cancel')
                                                            .click(function() {$alertContainer.empty();} )));
                                        });
            
            $btnToolbar
                .append($openLandingPage)
                .append($openHistory)
                .append($openProvenance)
                .append($download)
                .append($rename)
                .append($delete);
            
            return $btnToolbar;
        },
        
        
        renderObjectRowDiv: function(object_info, object_key) {
            var self = this;
            // object_info:
            // [0] : obj_id objid // [1] : obj_name name // [2] : type_string type
            // [3] : timestamp save_date // [4] : int version // [5] : username saved_by
            // [6] : ws_id wsid // [7] : ws_name workspace // [8] : string chsum
            // [9] : int size // [10] : usermeta meta
            var type_tokens = object_info[2].split('.')
            var type_module = type_tokens[0];
            var type = type_tokens[1].split('-')[0];
            var unversioned_full_type = type_module + '.' + type;
            var logo_name = "";
            if (_.has(this.data_icons, type)) {
              logo_name = this.data_icons[type];
            }
            else {
              logo_name = this.data_icons['DEFAULT'];
            }
            var logo_url = "static/kbase/images/data-icons/" + logo_name + ".png";
            var $logo = $('<div>')
                            .addClass("kb-data-list-logo")
                            .css({'background-image': 'url(' + logo_url + ')',
                                  'background-color':this.logoColorLookup(type),'cursor':'pointer'})
                            //.append(type.substring(0,1))
                            .click(function(e) {
                                e.stopPropagation();
                                self.insertViewer(object_key);
                            });
            var shortName = object_info[1]; var isShortened=false;
            if (shortName.length>this.options.max_name_length) {
                shortName = shortName.substring(0,this.options.max_name_length-3)+'...';
                isShortened=true;
            }
            var $name = $('<span>').addClass("kb-data-list-name").append(shortName)
                            .css({'cursor':'pointer'})
                            .click(function(e) {
                                e.stopPropagation();
                                self.insertViewer(object_key);
                            });
            if (isShortened) { $name.tooltip({title:object_info[1], placement:'bottom', delay: { show: 750, hide: 0 } }); }

            var $version = $('<span>').addClass("kb-data-list-version").append('v'+object_info[4]);
            var $type = $('<span>').addClass("kb-data-list-type").append(type);

            var $date = $('<span>').addClass("kb-data-list-date").append(this.getTimeStampStr(object_info[3]));
            var metadata = object_info[10];
            var metadataText = '';
            for(var key in metadata) {
                if (metadata.hasOwnProperty(key)) {
                    metadataText += '<tr><th>'+ key +'</th><td>'+ metadata[key] + '</td></tr>';
                }
            }
            if (type==='Genome') {
                if (metadata.hasOwnProperty('Name')) {
                    $type.text(type+': '+metadata['Name']);
                }
            }

            var $savedByUserSpan = $('<td>').addClass('kb-data-list-username-td');
            this.displayRealName(object_info[5],$savedByUserSpan);

            var $alertDiv = $('<div>').css({'text-align':'center','margin':'10px'});
            var typeLink = '<a href="'+this.options.landing_page_url+'spec/module/'+type_module+'" target="_blank">' +type_module+"</a>.<wbr>" +
                           '<a href="'+this.options.landing_page_url+'spec/type/'+object_info[2]+'" target="_blank">' +(type_tokens[1].replace('-','&#8209;')) + '.' + type_tokens[2] + '</a>';
            var $moreRow  = $('<div>').addClass("kb-data-list-more-div").hide()
                                .append($('<div>').css({'text-align':'center','margin':'5pt'})
                                            .append(self.addDataControls(object_info,$alertDiv)).append($alertDiv))
                                .append(
                                    $('<table style="width:100%;">')
                                        .append("<tr><th>Permament Id</th><td>" +object_info[6]+ "/" +object_info[0]+ "/" +object_info[4] + '</td></tr>')
                                        .append("<tr><th>Full Type</th><td>"+typeLink+'</td></tr>')
                                        .append($('<tr>').append('<th>Saved by</th>').append($savedByUserSpan))
                                        .append(metadataText));

            var $toggleAdvancedViewBtn = $('<span>').addClass("kb-data-list-more")//.addClass('btn btn-default btn-xs kb-data-list-more-btn')
                .hide()
                .html('<span class="fa fa-ellipsis-h" style="color:#999" aria-hidden="true"/>');
            var toggleAdvanced = function() {
                    if (self.selectedObject == object_info[0] && $moreRow.is(':visible')) {
                        // assume selection handling occurs before this is called
                        // so if we are now selected and the moreRow is visible, leave it...
                        return;
                    }
                    if ($moreRow.is(':visible')) {
                        $moreRow.slideUp('fast');
                        $toggleAdvancedViewBtn.show();
                    } else {
                        self.getRichData(object_info,$moreRow);
                        $moreRow.slideDown('fast');
                        $toggleAdvancedViewBtn.hide();
                    }
                };

            var $mainDiv  = $('<div>').addClass('kb-data-list-info').css({padding:'0px',margin:'0px'})
                                .append($name).append($version).append('<br>')
                                .append($type).append('<br>').append($date)
                                .append($toggleAdvancedViewBtn)
                                .click(
                                    function() {
                                        self.setSelected($(this).closest('.kb-data-list-obj-row'),object_info);
                                        toggleAdvanced();
                                    });

            var $topTable = $('<table>')
                             .css({'width':'100%','background':'#fff'})  // set background to white looks better on DnD
                             .append($('<tr>')
                                     .append($('<td>')
                                             .css({'width':'15%'})
                                             .append($logo))
                                     .append($('<td>')
                                             .append($mainDiv)));

            var $row = $('<div>').addClass('kb-data-list-obj-row')
                            .attr('kb-oid', object_key)
                            .append($('<div>').addClass('kb-data-list-obj-row-main')
                                        .append($topTable))<commit_msg>fixed DnD so oid is on element passed to DnD creation method<commit_after>/**
 * @author <NAME> <<EMAIL>>
 * @public
 */
(function( $, undefined ) {
    $.KBWidget({
        name: 'kbaseNarrativeDataList',
        parent: 'kbaseAuthenticatedWidget',
        version: '1.0.0',
        options: {
            ws_name: null, // must be the WS name, not the WS Numeric ID

            ws_url:"https://kbase.us/services/ws",
            landing_page_url: "/functional-site/#/", // !! always include trailing slash
            default_landing_page_url: "/functional-site/#/ws/json/", // ws_name/obj_name,
            
            user_name_fetch_url:"https://kbase.us/services/genome_comparison/users?usernames=",

            loadingImage: 'static/kbase/images/ajax-loader.gif',
            methodStoreURL: 'http://dev19.berkeley.kbase.us/narrative_method_store',

            ws_chunk_size:10000,  // this is the limit of the number of objects to retrieve from the ws on each pass
            ws_max_objs_to_fetch: 75000, // this is the total limit of the number of objects before we stop trying to get more
                                         // note that if there are more objects than this, then sorts/search filters may
                                         // not show accurate results

            objs_to_render_to_start:40, // initial number of rows to display
            objs_to_render_on_scroll:5, // 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

            max_objs_to_prevent_filter_as_you_type_in_search:50000, //if there are more than this # of objs, user must click search
                                                                    //instead of updating as you type

            max_objs_to_prevent_initial_sort:10000, // initial sort makes loading slower, so we can turn it off if
                                                    // there are more than this number of objects

            max_name_length:33,
            refresh_interval:30000,

            parentControlPanel: null
        },

        // private variables
        mainListPanelHeight : '340px',

        ws_name: null,
        ws: null,
        ws_last_update_timestamp: null,
        ws_obj_count: null,

        n_objs_rendered:0,

        ws_landing_page_map: {},
        real_name_lookup: {},

        $searchInput: null,
        $filterTypeSelect: null,
        availableTypes:{},

        $searchDiv: null,
        $sortByDiv: null,
        $filterTypeDiv: null,

        $addDataButton:null,
        $controllerDiv: null,
        $mainListDiv:null,
        mainListId:null,
        $loadingDiv:null,

        methClient: null,

        obj_list : [],
        obj_data : {}, // old style - type_name : info


    
        /**
         * @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;
            this.getLandingPageMap();  //start off this request so that we hopefully get something back right away

            this.$controllerDiv = $('<div>');
            this.$elem.append(this.$controllerDiv);
            this.renderController();
            this.$loadingDiv = $('<div>').addClass('kb-data-loading')
                                 .append('<img src="' + this.options.loadingImage + '">');
            this.$elem.append(this.$loadingDiv);
            this.mainListId=this.genUUID();
            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.$mainListDiv)
                                            .append(this.$addDataButton.hide());
            this.$elem.append($mainListDivContainer);

            if (window.kbconfig === undefined || window.kbconfig.urls === undefined ||
                window.kbconfig.data_icons === undefined) {
              // bail out now
              alert("Failed to load base configuration! Aborting narrative now.");
              window.location = "/"; //XXX: Need to load the error page!!
            }
            this.options.methodStoreURL = window.kbconfig.urls.narrative_method_store;
            this.options.ws_url = window.kbconfig.urls.workspace;
            this.data_icons = window.kbconfig.data_icons;

            if (this._attributes.auth) {
                this.ws = new Workspace(this.options.ws_url, this._attributes.auth);
            }
            setInterval(function(){self.refresh();}, this.options.refresh_interval); // check if there is new data every X ms

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

            this.showLoading();
            if (this.options.ws_name) {
                this.setWorkspace(this.options.ws_name);
            }

            this.methClient = new NarrativeMethodStore(this.options.methodStoreURL);

            return this;
        },

        setWorkspace : function(ws_name) {
            this.ws_name = ws_name;
            //this.ws_name = "janakacore"; // for testing a bigish workspace
            //this.ws_name = "KBasePublicGenomesV4"; // for testing a very big workspace
            this.refresh();
        },

        refresh: function() {
            var self = this;
            if (self.ws_name && self.ws) {
                self.ws.get_workspace_info({
                        workspace: this.ws_name
                    },
                    function(workspace_info) {
                        //[0] ws_id id, [1] ws_name workspace, [2] username owner, [3] timestamp moddate,
                        //[4] int object, [5] permission user_permission, [6] permission globalread,
                        //[7] lock_status lockstat, [8] usermeta metadata
                        //console.log('I have: '+self.ws_last_update_timestamp+ " remote has: "+workspace_info[3]);
                        if (self.ws_last_update_timestamp) {
                            if (self.ws_last_update_timestamp !== workspace_info[3]) {
                                self.ws_last_update_timestamp = workspace_info[3];
                                self.ws_obj_count = workspace_info[4];
                                self.reloadWsData();
                            } else {
                                //console.log('updating times');
                                self.refreshTimeStrings();
                            }
                        } else {
                            self.ws_last_update_timestamp = workspace_info[3];
                            self.ws_obj_count = workspace_info[4];
                            self.reloadWsData();
                        }
                    },
                    function(error) {
                        // don't worry about it, just do nothing...
                        self.$mainListDiv.show();
                        self.$mainListDiv.append("error: ");
                        self.$mainListDiv.append(error.error.message);
                        console.error(error);
                        self.hideLoading();
                    });
            } // else { we should probably do something if the user is not logged in or if the ws isn't set yet }
        },
        
        refreshSpecificObject: function() {
            
        },

        refreshTimeStrings: function() {
            var self = this; var newTime; var oldTime;
            if (self.objectList) {
                for(var i=0; i<self.objectList.length; i++) {
                    if(self.objectList[i].$div) {
                        newTime = self.getTimeStampStr(self.objectList[i].info[3]);
                        oldTime = self.objectList[i].$div.find('.kb-data-list-date').text();
                        if (newTime !== oldTime) {
                            self.objectList[i].$div.find('.kb-data-list-date').text(newTime);
                        }
                    }
                }
            }
        },

        reloadWsData: function () {
            var self = this;
            if (self.ws_name && self.ws) {
                // empty the existing object list first
                self.objectList = [];
                self.obj_data = {};
                self.availableTypes = {};

                self.getNextDataChunk(0);
            }
        },

        getNextDataChunk: function(skip) {
            var self = this;
            self.ws.list_objects({
                    workspaces : [self.ws_name],
                    includeMetadata: 1,
                    skip: skip,
                    limit: self.options.ws_chunk_size
                },
                function(infoList) {
                    // object_info:
                    // [0] : obj_id objid // [1] : obj_name name // [2] : type_string type
                    // [3] : timestamp save_date // [4] : int version // [5] : username saved_by
                    // [6] : ws_id wsid // [7] : ws_name workspace // [8] : string chsum
                    // [9] : int size // [10] : usermeta meta
                    for (var i=0; i<infoList.length; i++) {
                        // skip narrative objects
                        if (infoList[i][2].indexOf('KBaseNarrative') == 0) { continue; }
                        self.objectList.push(
                            {
                                $div:null, //self.renderObjectRowDiv(infoList[i]), // we defer rendering the div until it is shown
                                info:infoList[i],
                                attached:false
                            }
                        );
                        var typeKey = infoList[i][2].split("-")[0];
                        if (!(typeKey in self.obj_data)) {
                            self.obj_data[typeKey]=[];
                        }
                        self.obj_data[typeKey].push(infoList[i]);

                        var typeName = typeKey.split('.')[1];
                        if (!(typeName in self.availableTypes)) {
                            self.availableTypes[typeName] =
                                        {
                                            type:typeName,
                                            count:0
                                        };
                        }
                        self.availableTypes[typeName].count++;
                    }

                    // if we have more than 2k objects, make them hit enter to search...
                    self.$searchInput.off("input change blur");
                    self.$searchInput.on("change blur",function() { self.search(); });
                    if (self.objectList.length<=self.options.max_objs_to_prevent_filter_as_you_type_in_search) {
                        self.$searchInput.on("input",function() { self.search(); });
                    }

                    self.trigger('dataUpdated.Narrative');

                    //LOGIC: we keep trying to get more until we reach the ws_obj_count or untill the max
                    // fetch count option, UNLESS the last call returned nothing, in which case we stop.
                    //IMPORTANT NOTE: IN RARE CASES THIS DOES NOT GAURANTEE THAT WE GET ALL OBJECTS FROM
                    //THIS WS!!  IF THERE IS A CHUNK THAT RETURNED NOTHING, THERE STILL MAY BE MORE
                    //OBJECTS DUE TO A BUG IN THE WORKSPACE THAT INCLUDES OLD VERSIONS AND DELETED VERSIONS
                    //BEFORE FILTERING OUT THE NUMBER - A BETTER TEMP FIX WOULD BE TO LIMIT THE NUMBER OF
                    //RECURSIONS TO 2 or 3 MAYBE...
                    if (self.objectList.length < self.ws_obj_count
                            && self.objectList.length < self.options.ws_max_objs_to_fetch
                            && infoList.length>0) {
                        self.getNextDataChunk(skip+self.options.ws_chunk_size);
                    } else {
                        if (self.objectList.length<=self.options.max_objs_to_prevent_initial_sort) {
                            self.objectList.sort(function(a,b) {
                                    if (a.info[3] > b.info[3]) return -1; // sort by date
                                    if (a.info[3] < b.info[3]) return 1;  // sort by date
                                    return 0;
                                });
                            self.$elem.find('#nar-data-list-default-sort-label').addClass('active');
                            self.$elem.find('#nar-data-list-default-sort-option').attr('checked');
                        }
                    }

                    self.populateAvailableTypes();
                    self.renderList();
                    self.hideLoading();
                },
                function(error) {
                    self.$mainListDiv.show();
                    self.$mainListDiv.append("error: ");
                    self.$mainListDiv.append(error.error.message);
                    console.error(error);
                    self.hideLoading();
                });

        },

        getObjData: function(type, ignoreVersion) {
            if (type) {
                var dataSet = {};
                if (typeof type === 'string') {
                    type = [type];
                }
                for (var i=0; i<type.length; i++) {
                    if (this.obj_data[type[i]]) {
                        dataSet[type[i]]=this.obj_data[type[i]];
                    }
                }
                return dataSet;
            }
            return this.obj_data;
        },

        $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');
            } else {
                $selectedRow.addClass('kb-data-list-obj-row-selected');
                self.$currentSelectedRow = $selectedRow;
                self.selectedObject = object_info[0];
                self.trigger('filterMethods.Narrative','type:'+object_info[2].split('-')[0].split('.')[1]);
            }
        },
        
        
        addDataControls: function(object_info, $alertContainer) {
            var self = this;
            var $btnToolbar = $('<span>')
                                        .addClass('btn-toolbar')
                                        .attr('role', 'toolbar');
            
            var btnClasses = "btn btn-xs btn-default";
            var css = {'color':'#888'};
            
                                /*.append($('<div>').css({'text-align':'center','margin':'5pt'})
                                            .append('<a href="'+landingPageLink+'" target="_blank">'+
                                                        'explore data</a>&nbsp&nbsp|&nbsp&nbsp')
                                            .append('<a href="'+this.options.landing_page_url+'objgraphview/'+object_info[7] +'/'+object_info[1] +'" target="_blank">'+
                                                        'view provenance</a><br>'))*/
            
            var $openLandingPage = $('<span>')
                                        .tooltip({title:'Explore data', 'container':'#'+this.mainListId})
                                        .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.default_landing_page_url +object_info[7]+ '/' + object_info[1];
                                            if (self.ws_landing_page_map) {
                                                if (self.ws_landing_page_map[typeTokens[0]]) {
                                                    if (self.ws_landing_page_map[typeTokens[0]][typeTokens[1]]) {
                                                        landingPageLink = self.options.landing_page_url +
                                                            self.ws_landing_page_map[typeTokens[0]][typeTokens[1]] + "/" +
                                                            object_info[7]+ '/' + object_info[1];
                                                    }
                                                }
                                            }
                                            window.open(landingPageLink);
                                        });
                                        
            var $openHistory = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'View history to revert changes', 'container':'body'})
                                        .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) {
                                                        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'});
                                                            } else {
                                                                var revertRef = {wsid:history[k][6], objid:history[k][0], ver:history[k][4]};
                                                                $revertBtn.tooltip({title:'Revert to this version?', 'container':'body',placement:'bottom'})
                                                                    .click(function() {
                                                                        self.ws.revert_object(revertRef,
                                                                            function(reverted_obj_info) {
                                                                                self.refresh();
                                                                            }, function(error) {
                                                                                console.error(error);
                                                                                $alertContainer.empty();
                                                                                $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                            });
                                                                    })
                                                            }
                                                            $tbl.append($('<tr>')
                                                                        .append($('<td>').append($revertBtn))
                                                                        .append($('<td>').append('Saved by '+history[k][5]+'<br>'+self.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'}))
                                                                                );
                                                        }
                                                        $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'})
                                        .append($('<span>').addClass('fa fa-sitemap fa-rotate-90').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            window.open(self.options.landing_page_url+'objgraphview/'+object_info[7]+'/'+object_info[1]);
                                        });
            var $download = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Export / Download data', 'container':'body'})
                                        .append($('<span>').addClass('fa fa-download').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            $alertContainer.append('Coming soon');
                                        });
            
            var $rename = $('<span>')
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Rename data', 'container':'body'})
                                        .append($('<span>').addClass('fa fa-font').css(css))
                                        .click(function(e) {
                                            e.stopPropagation(); $alertContainer.empty();
                                            var $newNameInput = $('<input type="text">').addClass('form-control').val(object_info[1]);
                                            $alertContainer.append($('<div>')
                                                .append($('<div>').append("Warning: Apps using the old name may break."))
                                                .append($('<div>').append($newNameInput))
                                                .append($('<button>').addClass('kb-data-list-btn')
                                                            .append('Rename')
                                                            .click(function() {
                                                                if (self.ws_name && self.ws) {
                                                                    self.ws.rename_object({
                                                                            obj: {ref:object_info[6]+"/"+object_info[0]},
                                                                            new_name: $newNameInput.val()
                                                                        },
                                                                        function(renamed_info) {
                                                                            self.refresh();
                                                                        },
                                                                        function(error) {
                                                                            console.error(error);
                                                                            $alertContainer.empty();
                                                                            $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                        });
                                                                }
                                                            }))
                                                .append($('<button>').addClass('kb-data-list-cancel-btn')
                                                            .append('Cancel')
                                                            .click(function() {$alertContainer.empty();} )));
                                        });
            var $delete = $('<span>')   
                                        .addClass(btnClasses).css(css)
                                        .tooltip({title:'Delete data'})
                                        .append($('<span>').addClass('fa fa-trash-o').css(css))
                                        .click(function(e) {
                                            e.stopPropagation();
                                            $alertContainer.empty();
                                            $alertContainer.append($('<div>')
                                                .append($('<span>').append('Are you sure?'))
                                                .append($('<button>').addClass('kb-data-list-btn')
                                                            .append('Delete')
                                                            .click(function() {
                                                                if (self.ws_name && self.ws) {
                                                                    self.ws.rename_object({
                                                                            obj: {ref:object_info[6]+"/"+object_info[0]},
                                                                            new_name: object_info[1].split('-deleted-')[0] + "-deleted-"+(new Date()).getTime()
                                                                        },
                                                                        function(renamed_info) {
                                                                            self.ws.delete_objects([{ref:object_info[6]+"/"+object_info[0]}],
                                                                                function() {
                                                                                    self.refresh();
                                                                                },
                                                                                function(error) {
                                                                                    console.error(error);
                                                                                    $alertContainer.empty();
                                                                                    $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                                });
                                                                        },
                                                                        function(error) {
                                                                            console.error(error);
                                                                            $alertContainer.empty();
                                                                            $alertContainer.append($('<span>').css({'color':'#F44336'}).append("Error! "+error.error.message));
                                                                        });
                                                                }
                                                            }))
                                                .append($('<button>').addClass('kb-data-list-cancel-btn')
                                                            .append('Cancel')
                                                            .click(function() {$alertContainer.empty();} )));
                                        });
            
            $btnToolbar
                .append($openLandingPage)
                .append($openHistory)
                .append($openProvenance)
                .append($download)
                .append($rename)
                .append($delete);
            
            return $btnToolbar;
        },
        
        
        renderObjectRowDiv: function(object_info, object_key) {
            var self = this;
            // object_info:
            // [0] : obj_id objid // [1] : obj_name name // [2] : type_string type
            // [3] : timestamp save_date // [4] : int version // [5] : username saved_by
            // [6] : ws_id wsid // [7] : ws_name workspace // [8] : string chsum
            // [9] : int size // [10] : usermeta meta
            var type_tokens = object_info[2].split('.')
            var type_module = type_tokens[0];
            var type = type_tokens[1].split('-')[0];
            var unversioned_full_type = type_module + '.' + type;
            var logo_name = "";
            if (_.has(this.data_icons, type)) {
              logo_name = this.data_icons[type];
            }
            else {
              logo_name = this.data_icons['DEFAULT'];
            }
            var logo_url = "static/kbase/images/data-icons/" + logo_name + ".png";
            var $logo = $('<div>')
                            .addClass("kb-data-list-logo")
                            .css({'background-image': 'url(' + logo_url + ')',
                                  'background-color':this.logoColorLookup(type),'cursor':'pointer'})
                            //.append(type.substring(0,1))
                            .click(function(e) {
                                e.stopPropagation();
                                self.insertViewer(object_key);
                            });
            var shortName = object_info[1]; var isShortened=false;
            if (shortName.length>this.options.max_name_length) {
                shortName = shortName.substring(0,this.options.max_name_length-3)+'...';
                isShortened=true;
            }
            var $name = $('<span>').addClass("kb-data-list-name").append(shortName)
                            .css({'cursor':'pointer'})
                            .click(function(e) {
                                e.stopPropagation();
                                self.insertViewer(object_key);
                            });
            if (isShortened) { $name.tooltip({title:object_info[1], placement:'bottom', delay: { show: 750, hide: 0 } }); }

            var $version = $('<span>').addClass("kb-data-list-version").append('v'+object_info[4]);
            var $type = $('<span>').addClass("kb-data-list-type").append(type);

            var $date = $('<span>').addClass("kb-data-list-date").append(this.getTimeStampStr(object_info[3]));
            var metadata = object_info[10];
            var metadataText = '';
            for(var key in metadata) {
                if (metadata.hasOwnProperty(key)) {
                    metadataText += '<tr><th>'+ key +'</th><td>'+ metadata[key] + '</td></tr>';
                }
            }
            if (type==='Genome') {
                if (metadata.hasOwnProperty('Name')) {
                    $type.text(type+': '+metadata['Name']);
                }
            }

            var $savedByUserSpan = $('<td>').addClass('kb-data-list-username-td');
            this.displayRealName(object_info[5],$savedByUserSpan);

            var $alertDiv = $('<div>').css({'text-align':'center','margin':'10px'});
            var typeLink = '<a href="'+this.options.landing_page_url+'spec/module/'+type_module+'" target="_blank">' +type_module+"</a>.<wbr>" +
                           '<a href="'+this.options.landing_page_url+'spec/type/'+object_info[2]+'" target="_blank">' +(type_tokens[1].replace('-','&#8209;')) + '.' + type_tokens[2] + '</a>';
            var $moreRow  = $('<div>').addClass("kb-data-list-more-div").hide()
                                .append($('<div>').css({'text-align':'center','margin':'5pt'})
                                            .append(self.addDataControls(object_info,$alertDiv)).append($alertDiv))
                                .append(
                                    $('<table style="width:100%;">')
                                        .append("<tr><th>Permament Id</th><td>" +object_info[6]+ "/" +object_info[0]+ "/" +object_info[4] + '</td></tr>')
                                        .append("<tr><th>Full Type</th><td>"+typeLink+'</td></tr>')
                                        .append($('<tr>').append('<th>Saved by</th>').append($savedByUserSpan))
                                        .append(metadataText));

            var $toggleAdvancedViewBtn = $('<span>').addClass("kb-data-list-more")//.addClass('btn btn-default btn-xs kb-data-list-more-btn')
                .hide()
                .html('<span class="fa fa-ellipsis-h" style="color:#999" aria-hidden="true"/>');
            var toggleAdvanced = function() {
                    if (self.selectedObject == object_info[0] && $moreRow.is(':visible')) {
                        // assume selection handling occurs before this is called
                        // so if we are now selected and the moreRow is visible, leave it...
                        return;
                    }
                    if ($moreRow.is(':visible')) {
                        $moreRow.slideUp('fast');
                        $toggleAdvancedViewBtn.show();
                    } else {
                        self.getRichData(object_info,$moreRow);
                        $moreRow.slideDown('fast');
                        $toggleAdvancedViewBtn.hide();
                    }
                };

            var $mainDiv  = $('<div>').addClass('kb-data-list-info').css({padding:'0px',margin:'0px'})
                                .append($name).append($version).append('<br>')
                                .append($type).append('<br>').append($date)
                                .append($toggleAdvancedViewBtn)
                                .click(
                                    function() {
                                        self.setSelected($(this).closest('.kb-data-list-obj-row'),object_info);
                                        toggleAdvanced();
                                    });

            var $topTable = $('<table>').attr('kb-oid', object_key)
                             .css({'width':'100%','background':'#fff'})  // set background to white looks better on DnD
                             .append($('<tr>')
                                     .append($('<td>')
                                             .css({'width':'15%'})
                                             .append($logo))
                                     .append($('<td>')
                                             .append($mainDiv)));

            var $row = $('<div>').addClass('kb-data-list-obj-row')
                            .append($('<div>').addClass('kb-data-list-obj-row-main')
                                        .append($topTable))
{'fraction_non_alphanumeric': 0.11657935121977034, 'fraction_numerical': 0.006138540509075515, 'mean_word_length': 0.9158281586617666, 'pattern_counts': {'":': 0, '<': 223, '<?xml version=': 0, '>': 211, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 744, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5634679', 'n_tokens_mistral': 16229, 'n_tokens_neox': 15510, 'n_words': 2769}