79e2e0421ffa1203d75e66351a2046085129528e
jcasper
  Thu Jun 15 15:16:16 2017 -0700
hgHubConnect now displays search details and indexes description content, refs #13625

diff --git src/hg/hgHubConnect/hgHubConnect.c src/hg/hgHubConnect/hgHubConnect.c
index 23abdee..1b216e7 100644
--- src/hg/hgHubConnect/hgHubConnect.c
+++ src/hg/hgHubConnect/hgHubConnect.c
@@ -1,782 +1,1370 @@
 /* hgHubConnect - the CGI web-based program to select track data hubs to connect with. */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See README in this or parent directory for licensing information. */
 
 #include "common.h"
 #include "hash.h"
 #include "linefile.h"
 #include "errAbort.h"
 #include "errCatch.h"
 #include "hCommon.h"
 #include "dystring.h"
 #include "jksql.h"
 #include "cheapcgi.h"
 #include "htmshell.h"
 #include "hdb.h"
 #include "hui.h"
 #include "cart.h"
 #include "dbDb.h"
 #include "web.h"
 #include "trackHub.h"
 #include "hubConnect.h"
 #include "dystring.h"
 #include "hPrint.h"
 #include "jsHelper.h"
 #include "obscure.h"
 #include "hgConfig.h"
 #include "trix.h"
 #include "net.h"
-
+#include "hubSearchText.h"
 
 struct cart *cart;	/* The user's ui state. */
 struct hash *oldVars = NULL;
 
 static char *pageTitle = "Track Data Hubs";
 char *database = NULL;
 char *organism = NULL;
 
+struct hubOutputStructure
+    {
+    struct hubOutputStructure *next;
+    struct dyString *descriptionMatch;
+    struct genomeOutputStructure *genomes;
+    int genomeCount;
+    struct hash *genomeOutHash;
+    };
+
+struct genomeOutputStructure
+    {
+    struct genomeOutputStructure *next;
+    struct dyString *shortLabel;
+    struct dyString *descriptionMatch;
+    struct tdbOutputStructure *tracks;
+    struct dyString *assemblyLink;
+    char *genomeName;
+    int trackCount;
+    struct hash *tdbOutHash;
+    int hitCount;
+    };
+
+struct tdbOutputStructure
+    {
+    struct tdbOutputStructure *next;
+    struct dyString *shortLabel;
+    struct dyString *descriptionMatch;
+    struct dyString *configUrl;
+    struct tdbOutputStructure *children;
+    int childCount;
+    };
+
+struct hubEntry
+// for entries pulled from hubPublic
+    {
+    struct hubEntry *next;
+    char *hubUrl;
+    char *shortLabel;
+    char *longLabel;
+    char *dbList;
+    char *errorMessage;
+    int id;
+    char *descriptionUrl;
+    bool tableHasDescriptionField;
+    };
+
+struct hubEntry *hubEntryTextLoad(char **row, bool hasDescription)
+{
+struct hubEntry *ret;
+AllocVar(ret);
+ret->hubUrl = cloneString(row[0]);
+ret->shortLabel = cloneString(row[1]);
+ret->longLabel = cloneString(row[2]);
+ret->dbList = cloneString(row[3]);
+ret->errorMessage = cloneString(row[4]);
+ret->id = sqlUnsigned(row[5]);
+if (hasDescription)
+    ret->descriptionUrl = cloneString(row[6]);
+else
+    ret->descriptionUrl = NULL;
+return ret;
+}
+
+
 static void ourCellStart()
 {
 fputs("<TD>", stdout);  // do not add a newline
 }
 
 static void ourCellEnd()
 {
 puts("</TD>");
 }
 
 static void ourPrintCellLink(char *str, char *url)
 {
 ourCellStart();
 printf("<A class=\"cv\" HREF=\"%s\" TARGET=_BLANK>\n", url);
 if (str != NULL)
     fputs(str, stdout); // do not add a newline -- was causing trailing blanks get copied in cut and paste 
 puts("</A>");
 ourCellEnd();
 }
 
 static void ourPrintCell(char *str)
 {
 ourCellStart();
 if (str != NULL)
     fputs(str, stdout); // do not add a newline -- was causing trailing blanks get copied in cut and paste 
 ourCellEnd();
 }
 
 static char *removeLastComma(char *string)
 {
 if (string != NULL)
     {
     int len = strlen(string);
 
     if ( string[len - 1] == ',')
 	string[len - 1]  = 0;
     else if (len > 2 && endsWith(string,", "))
         string[len - 2] = 0;
     }
 return string;
 }
 
 #define GENLISTWIDTH 40
 static void printGenomeList(struct slName *genomes, int row)
 /* print supported assembly names from sl list */
 {
 /* List of associated genomes. */
 struct dyString *dy = newDyString(100);
 struct dyString *dyShort = newDyString(100);
 char *trimmedName = NULL;
 for(; genomes; genomes = genomes->next)
     {
     trimmedName = trackHubSkipHubName(genomes->name);
     dyStringPrintf(dy,"%s, ", trimmedName);
     if (dyShort->stringSize == 0 || (dyShort->stringSize+strlen(trimmedName)<=GENLISTWIDTH))
 	dyStringPrintf(dyShort,"%s, ", trimmedName);
     }
 char *genomesString = removeLastComma( dyStringCannibalize(&dy));
 char *genomesShort = removeLastComma( dyStringCannibalize(&dyShort));
 char tempHtml[1024+strlen(genomesString)+strlen(genomesShort)];
 if (strlen(genomesShort) > GENLISTWIDTH)  // If even the first element is too long, truncate it.
     genomesShort[GENLISTWIDTH] = 0;
 if (strlen(genomesShort)==strlen(genomesString))
     {
     safef(tempHtml, sizeof tempHtml, "%s", genomesString);
     }
 else
     {
     char id[256];
     safef(tempHtml, sizeof tempHtml, 
 	"<span id=Short%d>[+]&nbsp;%s...</span>"
 	"<span id=Full%d style=\"display:none\">[-]<br>%s</span>"
 	, row, genomesShort 
 	, row, genomesString);
 
     safef(id, sizeof id, "Short%d", row);
     jsOnEventByIdF("click", id,
 	"document.getElementById('Short%d').style.display='none';"
 	"document.getElementById('Full%d').style.display='inline';"
 	"return false;"
 	, row, row);
 
     safef(id, sizeof id, "Full%d", row);
     jsOnEventByIdF("click", id, 
 	"document.getElementById('Full%d').style.display='none';"
 	"document.getElementById('Short%d').style.display='inline';"
 	"return false;"
 	, row, row);
     }
 ourPrintCell(tempHtml);
-//ourPrintCell(removeLastComma( dyStringCannibalize(&dy)));
 }
 
 
 static void printGenomes(struct trackHub *thub, int row)
 /* print supported assembly names from trackHub */
 {
 /* List of associated genomes. */
 struct trackHubGenome *genomes = thub->genomeList;
 struct slName *list = NULL, *el;
 for(; genomes; genomes = genomes->next)
     {
     el = slNameNew(genomes->name);
     slAddHead(&list, el);
     }
 slReverse(&list);
 printGenomeList(list, row);
 }
 
 
 static void hgHubConnectUnlisted(struct hubConnectStatus *hubList, 
     struct hash *publicHash)
 /* Put up the list of unlisted hubs and other controls for the page. */
 /* uses publicHash to distingusih public hubs from unlisted ones */
 /* NOTE: Destroys hubList */
 {
 // put out the top of our page
 printf("<div id=\"unlistedHubs\" class=\"hubList\"> \n"
     "<table id=\"unlistedHubsTable\"> \n"
     "<thead><tr> \n"
 	"<th colspan=\"6\" id=\"addHubBar\"><label for=\"hubUrl\">URL:</label> \n"
 	"<input name=\"hubText\" id=\"hubUrl\" class=\"hubField\" "
 	    "type=\"text\" size=\"65\"> \n"
 	"<input name=\"hubAddButton\" id='hubAddButton' "
 		"class=\"hubField\" type=\"button\" value=\"Add Hub\">\n"
 	"</th> \n"
     "</tr> \n");
 jsOnEventById("click", "hubAddButton", 
     "var hubText = document.getElementById('hubUrl');"
     "hubText.value=$.trim(hubText.value);"
     "if(validateUrl($('#hubUrl').val())) { "
     " document.addHubForm.elements['hubUrl'].value=hubText.value;"
     " document.addHubForm.submit(); return true; } "
     "else { return false; }"
     );
 
 // count up the number of unlisted hubs we currently have
 int unlistedHubCount = 0;
 struct hubConnectStatus *unlistedHubList = NULL;
 struct hubConnectStatus *hub, *nextHub;
 
 for(hub = hubList; hub; hub = nextHub)
     {
     nextHub = hub->next;
     // if url is not in publicHash, it's unlisted */
     if (!((publicHash != NULL) && hashLookup(publicHash, hub->hubUrl)))
 	{
 	unlistedHubCount++;
 	slAddHead(&unlistedHubList, hub);
 	}
     }
 
 hubList = NULL;  // hubList no longer valid
 
 if (unlistedHubCount == 0)
     {
     // nothing to see here
     printf("<tr><td>No Unlisted Track Hubs</td></tr>");
     printf("</thead></table></div>");
     return;
     }
 
 // time to output the big table.  First the header
 printf(
     "<tr> "
 	"<th>Display</th> "
 	"<th>Hub Name</th> "
 	"<th>Description</th> "
 	"<th>Assemblies</th> "
     "</tr>\n"
     "</thead>\n");
 
 // start first row
 printf("<tbody>");
 
 char id[256];
 int count = 0;
 for(hub = unlistedHubList; hub; hub = hub->next)
     {
     char hubName[64];
     safef(hubName, sizeof(hubName), "%s%u", hgHubConnectHubVarPrefix, hub->id);
     if (!cartUsualBoolean(cart, hubName, FALSE))
 	continue;
 
     if (count)
 	webPrintLinkTableNewRow();  // ends last row and starts a new one
     count++;
 
     puts("<tr>");
 
     ourCellStart();
     safef(id, sizeof id, "hubDisconnectButton%d", count);
     printf("<input name=\"hubDisconnectButton\" id='%s' "
 	"class=\"hubDisconnectButton\" type=\"button\" value=\"Disconnect\">\n", id);
     jsOnEventByIdF("click", id, 
 	"document.disconnectHubForm.elements['hubId'].value='%d';"
 	"document.disconnectHubForm.submit(); return true;", hub->id);
     ourCellEnd();
 
     if (hub->trackHub != NULL)
 	{
 	ourPrintCellLink(hub->trackHub->shortLabel, hub->hubUrl);
 	}
     else
 	ourPrintCell("");
 
     if (!isEmpty(hub->errorMessage))
 	{
 	ourCellStart();
 	printf("<span class=\"hubError\">ERROR: %s </span>"
 	    "<a TARGET=_BLANK href=\"../goldenPath/help/hgTrackHubHelp.html#Debug\">Debug Help</a>\n", 
 	    hub->errorMessage);
 	
 	safef(id, sizeof id, "hubClearButton%d", count);
 	// give people a chance to clear the error 
 	printf("<input name=\"hubClearButton\"  id='%s' "
 		"class=\"hubButton\" type=\"button\" value=\"Retry Hub\">"
 		, id);
 	jsOnEventByIdF("click", id,
 	    "document.resetHubForm.elements['hubCheckUrl'].value='%s';"
 	    "document.resetHubForm.submit(); return true;", hub->hubUrl);
 	ourCellEnd();
 	}
     else if (hub->trackHub != NULL)
 	{
 	if (hub->trackHub->descriptionUrl != NULL)
 	    ourPrintCellLink(hub->trackHub->longLabel, hub->trackHub->descriptionUrl);
 	else
 	    ourPrintCell(hub->trackHub->longLabel);
 	}
     else
 	ourPrintCell("");
 
     if (hub->trackHub != NULL)
 	printGenomes(hub->trackHub, count);
     else
 	ourPrintCell("");
 
     puts("</tr>");
     }
 
 printf("</tbody></TABLE>\n");
 printf("</div>");
 }
 
+
 static void addPublicHubsToHubStatus(struct sqlConnection *conn, char *publicTable, char  *statusTable)
-/* add url's in the hubPublic table to the hubStatus table if they aren't there already */
+/* Add urls in the hubPublic table to the hubStatus table if they aren't there already */
 {
 char query[1024];
 sqlSafef(query, sizeof(query), "select hubUrl from %s where hubUrl not in (select hubUrl from %s)\n", publicTable, statusTable); 
 struct sqlResult *sr = sqlGetResult(conn, query);
 char **row;
 while ((row = sqlNextRow(sr)) != NULL)
     {
     char *errorMessage = NULL;
     char *url = row[0];
 
     // add this url to the hubStatus table
     hubFindOrAddUrlInStatusTable(database, cart, url, &errorMessage);
     }
 }
 
-struct hash *getUrlSearchHash(char *trixFile, char *hubSearchTerms)
-/* find hubs that match search term in trixFile */
+
+struct hubSearchText *getHubSearchResults(struct sqlConnection *conn, char *hubSearchTableName,
+        char *hubSearchTerms, bool checkLongText, char *dbFilter, struct hash *hubLookup)
+/* Find hubs, genomes, and tracks that match the provided search terms.
+ * Return all hits that satisfy the (optional) supplied assembly filter.
+ * if checkLongText is FALSE, skip searching within the long description text entries */
 {
-struct hash *urlSearchHash = newHash(5);
-struct trix *trix = trixOpen(trixFile);
-int trixWordCount = chopString(hubSearchTerms, " ", NULL, 0);
-char *trixWords[trixWordCount];
-trixWordCount = chopString(hubSearchTerms, " ", trixWords, trixWordCount);
+struct hubSearchText *hubSearchResultsList = NULL;
+struct dyString *query = dyStringNew(100);
+char *noLongText = NULL;
 
-struct trixSearchResult *tsList = trixSearch(trix, trixWordCount, trixWords, tsmExpand);
-for ( ; tsList != NULL; tsList = tsList->next)
-    hashStore(urlSearchHash, tsList->itemId);
+if (!checkLongText)
+    noLongText = cloneString("textLength = 'Short' and");
+else
+    noLongText = cloneString("");
 
-return urlSearchHash;
-}
+sqlDyStringPrintf(query, "select * from %s where %s match(text) against ('%s' in natural language mode)",
+        hubSearchTableName, noLongText, hubSearchTerms);
 
-static boolean outputPublicTable(struct sqlConnection *conn, char *publicTable, char *statusTable, struct hash **pHash)
-/* Put up the list of public hubs and other controls for the page. */
+struct sqlResult *sr = sqlGetResult(conn, dyStringContents(query));
+char **row;
+while ((row = sqlNextRow(sr)) != NULL)
     {
-char *trixFile = hReplaceGbdb(cfgOptionEnvDefault("HUBSEARCHTRIXFILE", "hubSearchTrixFile", "/gbdb/hubs/public.ix"));
-char *hubSearchTerms = cartOptionalString(cart, hgHubSearchTerms);
-char *cleanSearchTerms = cloneString(hubSearchTerms);
-int trixFd = netUrlOpen(trixFile);
-boolean haveTrixFile = (trixFd != -1);
-if (haveTrixFile)
-    close(trixFd);
-struct hash *urlSearchHash = NULL;
+    struct hubSearchText *hst = hubSearchTextLoadWithNullGiveContext(row, hubSearchTerms);
+    char *hubUrl = hst->hubUrl;
+    char *db = cloneString(hst->db);
+    tolowers(db);
+    if (isNotEmpty(dbFilter))
+        {
+        if (isNotEmpty(db))
+            {
+            if (stringIn(dbFilter, db) == NULL)
+                continue;
+            }
+        else
+            {
+            // no db in the hubSearchText means this is a top-level hub hit.
+            // filter by the db list associated with the hub instead
+            struct hubEntry *hubInfo = hashFindVal(hubLookup, hubUrl);
+            char *dbList = cloneString(hubInfo->dbList);
+            tolowers(dbList);
+            if (stringIn(dbFilter, dbList) == NULL)
+                continue;
+            }
+        }
+    // Add hst to the list to be returned
+    slAddHead(&hubSearchResultsList, hst);
+    }
+return hubSearchResultsList;
+}
 
-printf("<div id=\"publicHubs\" class=\"hubList\"> \n");
 
-// if we have a trix file, draw the search box
-if (haveTrixFile)
+void printSearchBox(char *hubSearchTerms, char *dbFilter)
+/* Create the text boxes for search and database filtering along with the required
+ * javscript */
 {
-    puts("Enter search terms to find in public track hub description pages:<BR>"
+printf("Enter search terms to find in public track hub description pages:<BR>"
         "<input name=\"hubSearchTerms\" id=\"hubSearchTerms\" class=\"hubField\" "
-	"type=\"text\" size=\"65\"> \n"
+        "type=\"text\" size=\"65\" value=\"%s\"> \n"
         "<input name=\"hubSearchButton\" id='hubSearchButton' "
-	    "class=\"hubField\" type=\"button\" value=\"Search Public Hubs\">\n");
+        "class=\"hubField\" type=\"button\" value=\"Search Public Hubs\">\n",
+        hubSearchTerms!=NULL?hubSearchTerms:"");
 jsOnEventById("click", "hubSearchButton",
         "document.searchHubForm.elements['hubSearchTerms'].value=$('#hubSearchTerms').val();"
         "document.searchHubForm.submit();return true;");
+printf("<br>\n");
+printf("Filter hubs by assembly: "
+        "<input name=\"%s\" id=\"hubDbFilter\" class=\"hubField\" "
+        "type=\"text\" size=\"10\" value=\"%s\"> \n"
+        "<input name=\"dbFilterButton\" id='dbFilterButton' "
+        "class=\"hubField\" type=\"button\" value=\"Filter assemblies\">\n",
+        hgHubDbFilter, dbFilter!=NULL?dbFilter:"");
+jsOnEventById("click", "dbFilterButton",
+        "document.dbFilterHubForm.elements['hubDbFilter'].value=$('#hubDbFilter').val();"
+        "document.dbFilterHubForm.submit();return true;");
 puts("<BR><BR>\n");
 }
 
-// if we have search terms, put out the line telling the user so
-if (haveTrixFile && !isEmpty(hubSearchTerms))
+
+void printSearchTerms(char *hubSearchTerms)
+/* Write out a reminder about the current search terms and a note about
+ * how to navigate detailed search results */
 {
 printf("Displayed list <B>restricted by search terms:</B> %s\n", hubSearchTerms);
 puts("<input name=\"hubDeleteSearchButton\" id='hubDeleteSearchButton' "
         "class=\"hubField\" type=\"button\" value=\"Show All Hubs\">\n");
 jsOnEventById("click", "hubDeleteSearchButton",
         "document.searchHubForm.elements['hubSearchTerms'].value='';"
         "document.searchHubForm.submit();return true;");
 puts("<BR><BR>\n");
+printf("When exploring the detailed search results for a hub, you may right-click "
+        "on an assembly or track line to open it in a new window\n");
+puts("<BR><BR>\n");
+}
 
-    strLower(cleanSearchTerms);
-    urlSearchHash = getUrlSearchHash(trixFile, cleanSearchTerms);
+
+void printHubListHeader()
+/* Write out the header for a list of hubs in its own table */
+{
+puts("<I>Clicking Connect redirects to the gateway page of the selected hub's default assembly.</I><BR>");
+printf("<table id=\"publicHubsTable\" class=\"hubList\"> "
+        "<thead><tr> "
+            "<th>Display</th> "
+            "<th>Hub Name</th> "
+            "<th>Description</th> "
+            "<th>Assemblies</th> "
+        "</tr></thead></table>\n");
 }
 
-// make sure all the public hubs are in the hubStatus table.
-addPublicHubsToHubStatus(conn, publicTable, statusTable);
 
-struct hash *publicHash = newHash(5);
+struct hash *buildPublicLookupHash(struct sqlConnection *conn, char *publicTable, char *statusTable,
+        struct hash **pHash)
+/* Return a hash linking hub URLs to struct hubEntries.  Also make pHash point to a hash that just stores
+ * the names of the public hubs (for use later when determining if hubs were added by the user) */
+{
+struct hash *hubLookup = newHash(5);
+struct hash *publicLookup = newHash(5);
 char query[512];
 bool hasDescription = sqlColumnExists(conn, publicTable, "descriptionUrl");
 if (hasDescription)
-    sqlSafef(query, sizeof(query), "select p.hubUrl,p.shortLabel,p.longLabel,p.dbList,s.errorMessage,s.id,p.descriptionUrl from %s p,%s s where p.hubUrl = s.hubUrl", 
+    sqlSafef(query, sizeof(query), "select p.hubUrl,p.shortLabel,p.longLabel,p.dbList,"
+            "s.errorMessage,s.id,p.descriptionUrl from %s p,%s s where p.hubUrl = s.hubUrl", 
 	    publicTable, statusTable); 
 else
-    sqlSafef(query, sizeof(query), "select p.hubUrl,p.shortLabel,p.longLabel,p.dbList,s.errorMessage,s.id from %s p,%s s where p.hubUrl = s.hubUrl", 
+    sqlSafef(query, sizeof(query), "select p.hubUrl,p.shortLabel,p.longLabel,p.dbList,"
+            "s.errorMessage,s.id from %s p,%s s where p.hubUrl = s.hubUrl", 
 	    publicTable, statusTable); 
 
 struct sqlResult *sr = sqlGetResult(conn, query);
 char **row;
-int count = 0;
-boolean gotAnyRows = FALSE;
-char jsId[256];
 while ((row = sqlNextRow(sr)) != NULL)
     {
-    ++count;
-    char *url = row[0], *shortLabel = row[1], *longLabel = row[2], 
-    	  *dbList = row[3], *errorMessage = row[4], *descriptionUrl = row[6];
-    int id = atoi(row[5]);
-
-    hashStore(publicHash, url);
-    if ((urlSearchHash != NULL) && (hashLookup(urlSearchHash, url) == NULL))
-	continue;
+    struct hubEntry *hubInfo = hubEntryTextLoad(row, hasDescription);
+    hubInfo->tableHasDescriptionField = hasDescription;
+    hashAddUnique(hubLookup, hubInfo->hubUrl, hubInfo);
+    hashStore(publicLookup, hubInfo->hubUrl);
+    }
+sqlFreeResult(&sr);
+*pHash = publicLookup;
+return hubLookup;
+}
 
-    struct slName *dbListNames = slNameListFromComma(dbList);
 
-    if (gotAnyRows)
-	webPrintLinkTableNewRow();
-    else
+void outputPublicTableRow(struct hubEntry *hubInfo, int count)
+/* Prints out a table row with basic information about a hub and a button
+ * to connect to that hub */
 {
-	/* output header */
-
-	puts("<I>Clicking Connect redirects to the gateway page of the selected hub's default assembly.</I><BR>");
-	printf("<table id=\"publicHubsTable\"> "
-	    "<thead><tr> "
-		"<th>Display</th> "
-		"<th>Hub Name</th> "
-		"<th>Description</th> "
-		"<th>Assemblies</th> "
-	    "</tr></thead>\n");
-
-	// start first row
-	printf("<tbody> <tr>");
-	gotAnyRows = TRUE;
-	}
-
+int id = hubInfo->id;
+char jsId[256];
+struct slName *dbListNames = slNameListFromComma(hubInfo->dbList);
+printf("<tr>\n");
 if (id != 0)
     {
     ourCellStart();
     char hubName[32];
     safef(hubName, sizeof(hubName), "%s%u", hgHubConnectHubVarPrefix, id);
     if (cartUsualBoolean(cart, hubName, FALSE))
         {
         safef(jsId, sizeof jsId, "hubDisconnectButton%d", count);
         printf("<input name=\"hubDisconnectButton\" id='%s' "
             "class=\"hubDisconnectButton\" type=\"button\" value=\"Disconnect\">\n", jsId);
         jsOnEventByIdF("click", jsId, 
             "document.disconnectHubForm.elements['hubId'].value= '%d';"
             "document.disconnectHubForm.submit();return true;", id);
         }
     else
         {
         // get first name off of list of supported databases
         char * name = dbListNames->name;
 
         // if the name isn't currently loaded, we assume it's a hub
         if (!hDbExists(name))
             {
             char buffer[512];
-
             safef(buffer, sizeof buffer, "hub_%d_%s",  id, name);
             name = cloneString(buffer);
             }
 
         safef(jsId, sizeof jsId, "hubConnectButton%d", count);
         printf("<input name=\"hubConnectButton\" id='%s' "
             "class=\"hubButton\" type=\"button\" value=\"Connect\">\n", jsId);
         jsOnEventByIdF("click", jsId, 
             "document.connectHubForm.elements['hubUrl'].value= '%s';"
             "document.connectHubForm.elements['db'].value= '%s';"
-		"document.connectHubForm.submit();return true;", url,name);
+            "document.connectHubForm.submit();return true;", hubInfo->hubUrl,name);
         }
 
     ourCellEnd();
     }
 else
-	errAbort("cannot get id for hub with url %s\n", url);
+    errAbort("cannot get id for hub with url %s\n", hubInfo->hubUrl);
 
-    ourPrintCellLink(shortLabel, url);
+ourPrintCellLink(hubInfo->shortLabel, hubInfo->hubUrl);
 
-    if (isEmpty(errorMessage))
+if (isEmpty(hubInfo->errorMessage))
     {
-	if (hasDescription && !isEmpty(descriptionUrl))
-	    ourPrintCellLink(longLabel, descriptionUrl);
+    if (hubInfo->tableHasDescriptionField && !isEmpty(hubInfo->descriptionUrl))
+        ourPrintCellLink(hubInfo->longLabel, hubInfo->descriptionUrl);
     else
-	    ourPrintCell(longLabel);
+        ourPrintCell(hubInfo->longLabel);
     }
 else
     {
     ourCellStart();
     printf("<span class=\"hubError\">ERROR: %s </span>"
         "<a href=\"../goldenPath/help/hgTrackHubHelp.html#Debug\">Debug Help</a>", 
-	    errorMessage);
+        hubInfo->errorMessage);
     safef(jsId, sizeof jsId, "hubClearButton%d", count);
     printf(
     "<input name=\"hubClearButton\" id='%s' "
             "class=\"hubButton\" type=\"button\" value=\"Retry Hub\">"
             , jsId);
     jsOnEventByIdF("click", jsId, 
         "document.resetHubForm.elements['hubCheckUrl'].value='%s';"
-	    "document.resetHubForm.submit();return true;", url);
+        "document.resetHubForm.submit();return true;", hubInfo->hubUrl);
     ourCellEnd();
     }
 
 printGenomeList(dbListNames, count); 
+printf("</tr>\n");
 }
-sqlFreeResult(&sr);
 
-if (gotAnyRows)
-    printf("</TR></tbody></TABLE>\n");
 
+void printSearchOutputForTrack(struct tdbOutputStructure *tdbOut)
+/* Write out a <li> entry for a search hit on a track, along with a nested
+ * <ul> for any included hits to subtracks */
+{
+printf("<li configLink='%s' nodeType='track'>\n", dyStringContents(tdbOut->configUrl));
+printf("%s", dyStringContents(tdbOut->shortLabel));
+if (tdbOut->childCount > 0)
+    printf(" (%d subtrack%s)", tdbOut->childCount, tdbOut->childCount==1?"":"s");
+printf("<br>\n");
+if (isNotEmpty(dyStringContents(tdbOut->descriptionMatch)))
+    {
+    printf("<span class='descriptionMatch'><em>%s</em></span>\n", dyStringContents(tdbOut->descriptionMatch));
+    }
+if (tdbOut->children != NULL)
+    {
+    struct tdbOutputStructure *child = tdbOut->children;
+    printf("<ul>\n");
+    while (child != NULL)
+        {
+        printSearchOutputForTrack(child);
+        child = child->next;
+        }
+    printf("</ul>\n");
+    }
+printf("</li>\n");
+}
+
+
+void printSearchOutputForGenome(struct genomeOutputStructure *genomeOut)
+/* Write out a chunk of search results for a genome as a <li>, with a nested ul
+ * element for hits to tracks within that genome */
+{
+printf("<li assemblyLink='%s' nodeType='assembly'>%s",
+        dyStringContents(genomeOut->assemblyLink), dyStringContents(genomeOut->shortLabel));
+if (genomeOut->trackCount > 0)
+    printf(" (%d track%s)", genomeOut->trackCount, genomeOut->trackCount==1?"":"s");
+
+if (isNotEmpty(dyStringContents(genomeOut->descriptionMatch)))
+    {
+    printf("<br>\n<em>Assembly Description:</em> %s\n", dyStringContents(genomeOut->descriptionMatch));
+    }
+if (genomeOut->tracks != NULL)
+    {
+    printf("<ul>\n");
+    struct tdbOutputStructure *tdbOut = genomeOut->tracks;
+    while (tdbOut != NULL)
+        {
+        printSearchOutputForTrack(tdbOut);
+        tdbOut = tdbOut->next;
+        }
+    printf("</ul>\n");
+    }
+printf("</li>\n");
+}
+
+
+struct trackHub *fetchTrackHub(struct hubEntry *hubInfo)
+/* Fetch the hub structure for a public hub, trapping the error
+ * if the hub cannot be reached */
+{
+struct errCatch *errCatch = errCatchNew();
+struct trackHub *hub = NULL;
+if (errCatchStart(errCatch))
+    {
+    char hubName[4096];
+    safef(hubName, sizeof(hubName), "hub_%d", hubInfo->id);
+    hub = trackHubOpen(hubInfo->hubUrl, hubName);
+    }
+errCatchEnd(errCatch);
+if (errCatch->gotError)
+    {
+    fprintf(stderr, "%s\n", errCatch->message->string);
+    }
+errCatchFree(&errCatch);
+return hub;
+}
+
+
+struct tdbOutputStructure *addOrUpdateTrackOut(char *track, struct genomeOutputStructure *genomeOut,
+        struct hash *tdbHash, struct trackHub *hub)
+/* If an output structure already exists for the track within genomeOut, return that.  Otherwise,
+ * create one for it and add it to genomeOut.  Any missing parent tracks are also added at
+ * the same time.
+ * tdbHash takes track names to the struct trackDb * for that track */
+{
+struct tdbOutputStructure *tdbOut = hashFindVal(genomeOut->tdbOutHash, track);
+if (tdbOut == NULL)
+    {
+    genomeOut->trackCount++;
+    AllocVar(tdbOut);
+    tdbOut->shortLabel = dyStringNew(0);
+    tdbOut->descriptionMatch = dyStringNew(0);
+    tdbOut->configUrl = dyStringNew(0);
+    struct trackDb *trackInfo = (struct trackDb *) hashFindVal(tdbHash, track);
+    if (trackInfo == NULL)
+        {
+        // Some tracks are prefixed with the hub name; try that
+        char withHubName[4096];
+        safef(withHubName, sizeof(withHubName), "%s_%s", hub->name, track);
+        trackInfo = hashMustFindVal(tdbHash, withHubName);
+        }
+    if (isNotEmpty(trackInfo->longLabel))
+        dyStringPrintf(tdbOut->shortLabel, "%s", trackInfo->longLabel);
+    else if (isNotEmpty(trackInfo->shortLabel))
+        dyStringPrintf(tdbOut->shortLabel, "%s", trackInfo->shortLabel);
+    else
+        dyStringPrintf(tdbOut->shortLabel, "%s", trackHubSkipHubName(trackInfo->track));
+
+    dyStringPrintf(tdbOut->configUrl, "../cgi-bin/hgTrackUi?hubUrl=%s&db=%s&g=%s&hgsid=%s", hub->url,
+            genomeOut->genomeName, trackInfo->track, cartSessionId(cart));
+
+    if (trackInfo->parent != NULL)
+        {
+        struct trackDb *parent = trackInfo->parent;
+        struct tdbOutputStructure *parentOut = addOrUpdateTrackOut(parent->track, genomeOut, tdbHash, hub);
+        slAddTail(&(parentOut->children), tdbOut);
+        parentOut->childCount++;
+        }
+    else
+        slAddTail(&(genomeOut->tracks), tdbOut);
+    hashAdd(genomeOut->tdbOutHash, track, tdbOut);
+    }
+return tdbOut;
+}
+
+
+void buildTdbHash(struct hash *tdbHash, struct trackDb *tdbList)
+/* Recursively add all tracks from tdbList to the hash (indexed by track),
+ * along with all parents and children of those tracks */
+{
+struct trackDb *tdb = tdbList;
+while (tdb != NULL)
+    {
+    hashAdd(tdbHash, tdb->track, tdb);
+    if (tdb->subtracks != NULL)
+        buildTdbHash(tdbHash, tdb->subtracks);
+    if (tdb->parent != NULL)
+        {
+        // supertracks might be omitted from tdbList, but are still referred to by parent links
+        if (hashFindVal(tdbHash, tdb->parent->track) == NULL)
+            hashAdd(tdbHash, tdb->parent->track, tdb->parent);
+        }
+    tdb = tdb->next;
+    }
+}
+
+
+struct hubOutputStructure *buildHubSearchOutputStructure(struct trackHub *hub,
+        struct hubSearchText *searchResults)
+/* Build a structure that contains the data for writing out the hub search results for this hub */
+{
+struct hubOutputStructure *hubOut = NULL;
+AllocVar(hubOut);
+hubOut->descriptionMatch = dyStringNew(0);
+hubOut->genomeOutHash = newHash(5);
+
+struct hash *tdbHashHash = newHash(5);  // takes genome names to trackDb hashes
+
+struct hubSearchText *hst = NULL;
+for (hst = searchResults; hst != NULL; hst = hst->next)
+    {
+    if (isEmpty(hst->db))
+        {
+        // must be a hit to the hub itself, not an assembly or track within it
+        if (hst->textLength == hubSearchTextLong)
+            {
+            dyStringPrintf(hubOut->descriptionMatch, "%s", hst->text);
+            }
+        continue;
+        }
+
+    char *db = cloneString(hst->db);
+    struct trackHubGenome *genome = hashFindVal(hub->genomeHash, db);
+    if (genome == NULL)
+        {
+        // assembly hub genomes are stored with a prefix; try that
+        char withHubName[4096];
+        safef(withHubName, sizeof(withHubName), "%s_%s", hub->name, db);
+        genome = hashMustFindVal(hub->genomeHash, withHubName);
+        }
+    struct genomeOutputStructure *genomeOut = hashFindVal(hubOut->genomeOutHash, db);
+    if (genomeOut == NULL)
+        {
+        AllocVar(genomeOut);
+        genomeOut->tdbOutHash = newHash(5);
+        genomeOut->descriptionMatch = dyStringNew(0);
+        genomeOut->shortLabel = dyStringNew(0);
+        genomeOut->assemblyLink = dyStringNew(0);
+        dyStringPrintf(genomeOut->assemblyLink, "../cgi-bin/hgTracks?hubUrl=%s&db=%s&hgsid=%s",
+                hub->url, genome->name, cartSessionId(cart));
+        char *name = trackHubSkipHubName(genome->name);
+        if (isNotEmpty(genome->description))
+            dyStringPrintf(genomeOut->shortLabel, "%s (%s)", genome->description, name);
+        else if (isNotEmpty(genome->organism))
+            dyStringPrintf(genomeOut->shortLabel, "%s %s", genome->organism, name);
+        else
+            dyStringPrintf(genomeOut->shortLabel, "%s", name);
+        genomeOut->genomeName = cloneString(genome->name);
+        hashAdd(hubOut->genomeOutHash, db, genomeOut);
+        slAddTail(&(hubOut->genomes), genomeOut);
+        hubOut->genomeCount++;
+        }
+
+    if (isNotEmpty(hst->track))
+        {
+        // Time to add a track! (or add info to one, maybe)
+        struct hash *tdbHash = (struct hash *) hashFindVal(tdbHashHash, db);
+        if (tdbHash == NULL)
+            {
+            tdbHash = newHash(5);
+            hashAdd(tdbHashHash, db, tdbHash);
+            struct trackDb *tdbList = trackHubTracksForGenome(hub, genome);
+            tdbList = trackDbLinkUpGenerations(tdbList);
+            tdbList = trackDbPolishAfterLinkup(tdbList, db);
+            trackHubPolishTrackNames(hub, tdbList);
+            buildTdbHash(tdbHash, tdbList);
+            }
+        struct tdbOutputStructure *tdbOut = addOrUpdateTrackOut(hst->track, genomeOut, tdbHash, hub);
+        if (hst->textLength == hubSearchTextLong)
+            dyStringPrintf(tdbOut->descriptionMatch, "%s", hst->text);
+        }
+    }
+return hubOut;
+}
+
+
+static void printOutputForHub(struct hubEntry *hubInfo, struct hubSearchText *hubSearchResult, int count)
+/* Given a hub's info and a structure listing the search hits within the hub, first print
+ * a basic line of hub information with a "connect" button.  Then, if the search results
+ * are non-NULL, write out information about the genomes and tracks from the search hits that
+ * match the db filter.
+ * If there are no search results to print, the basic hub lines are combined into a single HTML table
+ * that is defined outside this function.
+ * Otherwise, each hub line is printed in its own table followed by a <ul> containing details
+ * about the search results. */
+{
+if (hubSearchResult != NULL)
+    printf("<table class='hubList'><tbody>\n");
+outputPublicTableRow(hubInfo, count);
+if (hubSearchResult != NULL)
+    {
+    printf("</tbody></table>\n");
+
+    struct trackHub *hub = fetchTrackHub(hubInfo);
+    struct hubOutputStructure *hubOut = buildHubSearchOutputStructure(hub, hubSearchResult);
+    if (dyStringIsEmpty(hubOut->descriptionMatch) && (hubOut->genomes == NULL))
+        return; // no detailed search results; hit must have been to hub short label or something   
+
+    printf("<div class=\"hubTdbTree\">\n");
+    printf("<ul>\n");
+    printf("<li>Search details ...\n<ul>\n");
+    if (isNotEmpty(dyStringContents(hubOut->descriptionMatch)))
+        printf("<li>Hub Description:&nbsp<span class='descriptionMatch'><em>%s</em></span></li>\n", dyStringContents(hubOut->descriptionMatch));
+
+    struct genomeOutputStructure *genomeOut = hubOut->genomes;
+    if (genomeOut != NULL)
+        {
+        printf("<li>%d Matching Assembl%s\n<ul>\n", hubOut->genomeCount, hubOut->genomeCount==1?"y":"ies");
+        while (genomeOut != NULL)
+            {
+            printSearchOutputForGenome(genomeOut);
+            genomeOut = genomeOut->next;
+            }
+        printf("</ul></li>\n");
+        }
+    printf("</ul></li></ul></div>\n");
+    }
+}
+
+
+void printHubList(struct slName *hubsToPrint, struct hash *hubLookup, struct hash *searchResultHash)
+/* Print out a list of hubs, possibly along with search hits to those hubs.
+ * hubLookup takes hub URLs to struct hubEntry
+ * searchResultHash takes hub URLs to struct hubSearchText * (list of hits on that hub)
+ */
+{
+int count = 0;
+int udcTimeoutVal = udcCacheTimeout();
+char *udcOldDir = cloneString(udcDefaultDir());
+udcSetDefaultDir("/hive/users/jcasper/hubCrawl/");
+udcSetCacheTimeout(1<<30);
+if (hubsToPrint != NULL)
+    {
+    printHubListHeader();
+
+    if (searchResultHash == NULL) // if not displaying search results, join the hub <tr>s into one table
+        printf("<table class='hubList'><tbody>\n");
+    struct slName *thisHubName = NULL;
+    for (thisHubName = hubsToPrint; thisHubName != NULL; thisHubName = thisHubName->next)
+        {
+        struct hubEntry *hubInfo = (struct hubEntry *) hashFindVal(hubLookup, thisHubName->name);
+        if (hubInfo == NULL)
+            {
+            /* This shouldn't happen often, but maybe the search hits list was built from an outdated
+             * search text file that includes hubs for which no info is available.
+             * Skip this hub. */
+            continue;
+            }
+        struct hubSearchText *searchResult = NULL;
+        if (searchResultHash != NULL)
+            {
+            searchResult = (struct hubSearchText *) hashMustFindVal(searchResultHash, thisHubName->name);
+            }
+        printOutputForHub(hubInfo, searchResult, count);
+        count++;
+        }
+    if (searchResultHash == NULL)
+        printf("</tbody></table>\n");
+    }
+udcSetCacheTimeout(udcTimeoutVal);
+udcSetDefaultDir(udcOldDir);
+if (hubsToPrint != NULL)
+    {
+    /* Write out the list of hubs in a single table inside a div that will be hidden by
+     * javascript.  This table is used (before being hidden) to set common column widths for
+     * the individual hub tables when they're split by detailed search results. */
+    printf("<div id='hideThisDiv'>\n");
+    printf("<table class='hubList' id='hideThisTable'><tbody>\n");
+    struct slName *thisHubName = NULL;
+    for (thisHubName = hubsToPrint; thisHubName != NULL; thisHubName = thisHubName->next)
+        {
+        struct hubEntry *hubInfo = (struct hubEntry *) hashFindVal(hubLookup, thisHubName->name);
+        if (hubInfo == NULL)
+            {
+            continue;
+            }
+        printOutputForHub(hubInfo, NULL, count);
+        count++;
+        }
+    printf("</tbody></table>\n");
+    printf("</div>\n");
+    }
+
+jsInline(
+        "function lineUpCols()\n"
+        "    {\n"
+        "    var tableList = $('table.hubList');\n"
+        "    if (tableList.length == 0)\n"
+        "        return;\n"
+        "    var colWidths = new Array();\n"
+        "    var combinedTrackTable = $('#hideThisTable');\n"
+        "    for (i=0; i<combinedTrackTable[0].rows[0].cells.length; i++)\n"
+        "        colWidths[i] = combinedTrackTable[0].rows[0].cells[i].clientWidth;\n"
+        "    $('#hideThisDiv')[0].style.display = 'none';\n"
+        "    for(i=0; i<tableList.length; i++)\n"
+        "        {\n"
+        "        for(j=0; j<tableList[i].rows[0].cells.length; j++)\n"
+        "            tableList[i].rows[0].cells[j].style.width = colWidths[j]+'px';\n"
+        "        }\n"
+        "    }\n"
+        "window.onload = lineUpCols();\n"
+        );
+}
+
+
+static bool outputPublicTable(struct sqlConnection *conn, char *publicTable, char *statusTable,
+        struct hash **pHash)
+/* Put up the list of public hubs and other controls for the page. */
+{
+char *hubSearchTerms = cartOptionalString(cart, hgHubSearchTerms);
+char *cleanSearchTerms = cloneString(hubSearchTerms); // only cleaned by tolowers() at the moment
+char *dbFilter = cartOptionalString(cart, hgHubDbFilter);
+char *lcDbFilter = cloneString(dbFilter);
+if (isNotEmpty(lcDbFilter))
+    tolowers(lcDbFilter);
+
+// make sure all the public hubs are in the hubStatus table.
+addPublicHubsToHubStatus(conn, publicTable, statusTable);
+
+// build full public hub lookup hash, taking each URL to struct hubEntry * for that hub
+struct hash *hubLookup = buildPublicLookupHash(conn, publicTable, statusTable, pHash);
+
+printf("<div id=\"publicHubs\" class=\"hubList\"> \n");
+
+char *hubSearchTableName = cfgOptionDefault("hubSearchTextTable", "hubSearchText");
+int searchEnabled = sqlTableExists(conn, hubSearchTableName);
+if (searchEnabled)
+    printSearchBox(hubSearchTerms, dbFilter);
+
+struct hash *searchResultHash = NULL;
+struct slName *hubsToPrint = NULL;
+if (searchEnabled && !isEmpty(hubSearchTerms))
+    {
+    printSearchTerms(hubSearchTerms);
+    if (isNotEmpty(cleanSearchTerms))
+        tolowers(cleanSearchTerms);
+    // Forcing checkDescriptions to TRUE right now, but we might want to add this as a
+    // checkbox option for users in the near future.
+    bool checkDescriptions = TRUE;
+    struct hubSearchText *hubSearchResults = getHubSearchResults(conn, hubSearchTableName,
+            cleanSearchTerms, checkDescriptions, lcDbFilter, hubLookup);
+    searchResultHash = newHash(5);
+    struct hubSearchText *hst = hubSearchResults;
+    while (hst != NULL)
+        {
+        struct hubSearchText *nextHst = hst->next;
+        hst->next = NULL;
+        struct hashEl *hubHashEnt = hashLookup(searchResultHash, hst->hubUrl);
+        if (hubHashEnt == NULL)
+            {
+            slNameAddHead(&hubsToPrint, hst->hubUrl);
+            hashAdd(searchResultHash, hst->hubUrl, hst);
+            }
+        else
+            slAddTail(&(hubHashEnt->val), hst);
+        hst = nextHst;
+        }
+    }
+else
+    {
+    // There is no active search, so just add all hubs to the list
+    struct hashEl *hel;
+    struct hashEl *helList;
+    helList = hashElListHash(hubLookup);
+    for (hel = helList; hel != NULL; hel = hel->next)
+        {
+        if (isNotEmpty(lcDbFilter))
+            {
+            struct hubEntry *hubEnt = (struct hubEntry *) hel->val;
+            char *lcDbList = cloneString(hubEnt->dbList);
+            if (isNotEmpty(lcDbList))
+                tolowers(lcDbList);
+            if ((lcDbList == NULL) || (stringIn(lcDbFilter, lcDbList) == NULL))
+                continue;
+            }
+        slNameAddHead(&hubsToPrint, hel->name);
+        }
+    }
+slReverse(&hubsToPrint);
+
+printHubList(hubsToPrint, hubLookup, searchResultHash);
 printf("</div>");
-*pHash = publicHash;
-return gotAnyRows;
+return (hubsToPrint != NULL);
 }
 
 
 struct hash *hgHubConnectPublic()
 /* Put up the list of public hubs and other controls for the page. */
 {
 struct hash *retHash = NULL;
 struct sqlConnection *conn = hConnectCentral();
 char *publicTable = cfgOptionEnvDefault("HGDB_HUB_PUBLIC_TABLE", 
 	hubPublicTableConfVariable, defaultHubPublicTableName);
 char *statusTable = cfgOptionEnvDefault("HGDB_HUB_STATUS_TABLE", 
 	hubStatusTableConfVariable, defaultHubStatusTableName);
 if (!(sqlTableExists(conn, publicTable) && 
 	outputPublicTable(conn, publicTable,statusTable, &retHash)) )
     {
     printf("<div id=\"publicHubs\" class=\"hubList\"> \n");
     printf("No Public Track Hubs found that match search criteria.<BR>");
     printf("</div>");
     }
 hDisconnectCentral(&conn);
 
 return retHash;
 }
 
+
 static void tryHubOpen(unsigned id)
 /* try to open hub, leaks trackHub structure */
 {
 /* try opening this again to reset error */
 struct sqlConnection *conn = hConnectCentral();
 struct errCatch *errCatch = errCatchNew();
 struct hubConnectStatus *hub = NULL;
 if (errCatchStart(errCatch))
     hub = hubConnectStatusForId(conn, id);
 errCatchEnd(errCatch);
 if (errCatch->gotError)
     hubUpdateStatus( errCatch->message->string, NULL);
 else
     hubUpdateStatus(NULL, hub);
 errCatchFree(&errCatch);
 
 hDisconnectCentral(&conn);
 }
 
 
 static int doRedirect(struct cart *theCart)
 {
 struct hubConnectStatus *hub = hubConnectNewHub();
 if (hub == NULL)
     return 0;
 
 char headerText[1024];
 
 char *errorMessage;
 hubFindOrAddUrlInStatusTable(database, cart, hub->hubUrl, &errorMessage);
 
 // if there is an error message, we stay in hgHubConnect
 if (errorMessage != NULL)
     return 0;
 
 getDbAndGenome(cart, &database, &organism, oldVars);
 
 int redirDelay = 3;
 printf( "<META HTTP-EQUIV=\"REFRESH\" CONTENT=\"%d;URL=%s?%s\">",
 	  redirDelay,"../cgi-bin/hgGateway",cartSidUrlString(cart));
 safef(headerText, sizeof(headerText), "Hub Connect Successful");
 cartWebStart(cart, NULL, "%s", headerText);
 
 hPrintf("You will be automatically redirected to the gateway page for this hub's default database "
     "(<A HREF=\"../cgi-bin/hgGateway?%s\">%s</A>) in %d seconds.<BR><BR>",
 	  cartSidUrlString(cart),trackHubSkipHubName(database),redirDelay);
 
 struct trackHub *tHub = hub->trackHub;
 if (tHub->email != NULL)
     {
     hPrintf("<B>This hub is provided courtesy of <A HREF=\"mailto:%s\">%s</A>.</B> Please contact them with any questions.", tHub->email, tHub->email);
     }
 
 hPrintf("<BR><BR>");
 hPrintf("Hub: %s<BR><BR>", tHub->longLabel);
 hPrintf("Hub Genomes: ");
 struct trackHubGenome *genomeList = tHub->genomeList;
 
-boolean firstTime = TRUE;
+bool firstTime = TRUE;
 for(; genomeList; genomeList = genomeList->next)
     {
     if (!firstTime)
 	hPrintf(",");
     firstTime = FALSE;
     hPrintf("<A href=\"../cgi-bin/hgTracks?db=%s&%s\">%s</A>",genomeList->name, 
 	cartSidUrlString(cart),trackHubSkipHubName(genomeList->name));
     }
 hPrintf("<BR><BR>");
 return 1;
 }
 
 static void doResetHub(struct cart *theCart)
 {
 char *url = cartOptionalString(cart, hgHubCheckUrl);
 
 if (url != NULL)
     {
     udcSetCacheTimeout(1);
     unsigned id = hubResetError(url);
     tryHubOpen(id);
     }
 else
     errAbort("must specify url in %s\n", hgHubDataText);
 }
 
 static void doClearHub(struct cart *theCart)
 {
 char *url = cartOptionalString(cart, hgHubDataText);
 
 printf("<pre>clearing hub %s\n",url);
 if (url != NULL)
     hubClearStatus(url);
 else
     errAbort("must specify url in %s\n", hgHubDataText);
 printf("<pre>Completed\n");
 }
 
 
 static void checkTrackDbs(struct hubConnectStatus *hubList)
 {
 struct hubConnectStatus *hub = hubList;
 
 for(; hub; hub = hub->next)
     {
     struct errCatch *errCatch = errCatchNew();
     if (errCatchStart(errCatch))
 	{
 	hubAddTracks(hub, database);
 	}
     errCatchEnd(errCatch);
     if (errCatch->gotError)
 	{
 	hub->errorMessage = cloneString(errCatch->message->string);
 	hubUpdateStatus( errCatch->message->string, hub);
 	}
     else
 	hubUpdateStatus(NULL, hub);
     }
 }
 
 void doMiddle(struct cart *theCart)
 /* Write header and body of html page. */
 {
 cart = theCart;
 
 if(cgiIsOnWeb())
     checkForGeoMirrorRedirect(cart);
 
 if (cartVarExists(cart, hgHubDoClear))
     {
     doClearHub(cart);
     cartWebEnd();
     return;
     }
 
 if (cartVarExists(cart, hgHubCheckUrl))
     {
     doResetHub(cart);
     }
 
 if (cartVarExists(cart, hgHubDoRedirect))
     {
     if (doRedirect(cart))
 	{
 	cartWebEnd();
 	return;
 	}
     }
 
 cartWebStart(cart, NULL, "%s", pageTitle);
-jsIncludeFile("jquery.js", NULL);
+printf(
+"<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.4/themes/default/style.min.css\" />\n"
+"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min.js\"></script>\n"
+"<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.4/jstree.min.js\"></script>\n"
+"<style>.jstree-default .jstree-anchor { height: initial; } </style>\n"
+);
 jsIncludeFile("utils.js", NULL);
 jsIncludeFile("jquery-ui.js", NULL);
-
 webIncludeResourceFile("jquery-ui.css");
-
 jsIncludeFile("ajax.js", NULL);
 jsIncludeFile("hgHubConnect.js", NULL);
+webIncludeResourceFile("hgHubConnect.css");
 jsIncludeFile("jquery.cookie.js", NULL);
 
 printf("<div id=\"hgHubConnectUI\"> <div id=\"description\"> \n");
 printf(
     "<P>Track data hubs are collections of external tracks that can be imported into the UCSC Genome Browser. "
     "Hub tracks show up under the hub's own blue label bar on the main browser page, "
     "as well as on the configure page. For more information, see the "
     "<A HREF=\"../goldenPath/help/hgTrackHubHelp.html\" TARGET=_blank>"
     "User's Guide</A>."
     "To import a public hub click its \"Connect\" button below.</P>"
     "<P><B>NOTE: Because Track Hubs are created and maintained by external sources,"
     " UCSC is not responsible for their content.</B></P>"
    );
 printf("</div>\n");
 
 getDbAndGenome(cart, &database, &organism, oldVars);
 
 char *survey = cfgOptionEnv("HGDB_HUB_SURVEY", "hubSurvey");
 char *surveyLabel = cfgOptionEnv("HGDB_HUB_SURVEY_LABEL", "hubSurveyLabel");
 
 if (survey && differentWord(survey, "off"))
     hPrintf("<span style='background-color:yellow;'><A HREF='%s' TARGET=_BLANK><EM><B>%s</EM></B></A></span>\n", survey, surveyLabel ? surveyLabel : "Take survey");
 hPutc('\n');
 
 // grab all the hubs that are listed in the cart
 struct hubConnectStatus *hubList =  hubConnectStatusListFromCartAll(cart);
 
 checkTrackDbs(hubList);
 
 // here's a little form for the add new hub button
 printf("<FORM ACTION=\"%s\" NAME=\"addHubForm\">\n",  "../cgi-bin/hgHubConnect");
 cgiMakeHiddenVar("hubUrl", "");
 cgiMakeHiddenVar( hgHubDoFirstDb, "on");
 cgiMakeHiddenVar( hgHubDoRedirect, "on");
 cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
 cartSaveSession(cart);
 puts("</FORM>");
 
 // this is the form for the connect hub button
 printf("<FORM ACTION=\"%s\" NAME=\"connectHubForm\">\n",  "../cgi-bin/hgHubConnect");
 cgiMakeHiddenVar("hubUrl", "");
 cgiMakeHiddenVar("db", "");
 cgiMakeHiddenVar( hgHubDoRedirect, "on");
 cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
 cartSaveSession(cart);
 puts("</FORM>");
 
 // this is the form for the disconnect hub button
 printf("<FORM ACTION=\"%s\" NAME=\"disconnectHubForm\">\n",  "../cgi-bin/hgHubConnect");
 cgiMakeHiddenVar("hubId", "");
 cgiMakeHiddenVar(hgHubDoDisconnect, "on");
 cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
 cartSaveSession(cart);
 puts("</FORM>");
 
 // this is the form for the reset hub button
 printf("<FORM ACTION=\"%s\" NAME=\"resetHubForm\">\n",  "../cgi-bin/hgHubConnect");
 cgiMakeHiddenVar(hgHubCheckUrl, "");
 cartSaveSession(cart);
 puts("</FORM>");
 
 // this is the form for the search hub button
 printf("<FORM ACTION=\"%s\" NAME=\"searchHubForm\">\n",  "../cgi-bin/hgHubConnect");
 cgiMakeHiddenVar(hgHubSearchTerms, "");
 cgiMakeHiddenVar(hgHubDoSearch, "on");
 cartSaveSession(cart);
 puts("</FORM>");
 
+// this is the form for the search hub button
+printf("<FORM ACTION=\"%s\" NAME=\"dbFilterHubForm\">\n",  "../cgi-bin/hgHubConnect");
+cgiMakeHiddenVar(hgHubDbFilter, "");
+cgiMakeHiddenVar(hgHubDoFilter, "on");
+cartSaveSession(cart);
+puts("</FORM>");
+
 // ... and now the main form
 printf("<FORM ACTION=\"%s\" METHOD=\"POST\" NAME=\"mainForm\">\n", "../cgi-bin/hgGateway");
 cartSaveSession(cart);
 
 // we have two tabs for the public and unlisted hubs
 printf("<div id=\"tabs\">"
        "<ul> <li><a href=\"#publicHubs\">Public Hubs</a></li>"
        "<li><a href=\"#unlistedHubs\">My Hubs</a></li> "
        "</ul> ");
 
 struct hash *publicHash = hgHubConnectPublic();
 hgHubConnectUnlisted(hubList, publicHash);
 printf("</div>");
 
 printf("<div class=\"tabFooter\">");
 
 char *emailAddress = cfgOptionDefault("hub.emailAddress","genome@soe.ucsc.edu");
 printf("<span class=\"small\">"
     "Contact <A HREF=\"mailto:%s\">%s</A> to add a public hub."
     "</span>\n", emailAddress,emailAddress);
 printf("</div>");
 
 cgiMakeHiddenVar(hgHubConnectRemakeTrackHub, "on");
 
 puts("</FORM>");
 printf("</div>\n");
+
+jsInline(
+"var hubSearchTree = (function() {\n"
+"    // Effectively global vars set by init\n"
+"    var treeDiv;        // Points to div we live in\n"
+"\n"
+"    function hubSearchTreeContextMenuHandler (node, callback) {\n"
+"        var nodeType = node.li_attr.nodetype;\n"
+"        if (nodeType == 'track') {\n"
+"            callback({\n"
+"               'openConfig': {\n"
+"                   'label' : 'Configure this track',\n"
+"                   'action' : function () {window.open(node.li_attr.configlink, '_blank'); }\n"
+"               }\n"
+"            });\n"
+"        }\n"
+"        else if (nodeType == 'assembly') {\n"
+"            callback({\n"
+"               'openConfig': {\n"
+"                   'label' : 'Open this assembly',\n"
+"                   'action' : function () {window.open(node.li_attr.assemblylink, '_blank'); }\n"
+"               }\n"
+"            });\n"
+"        }\n"
+"    }"
+"    function init() {\n"
+"       $.jstree.defaults.core.themes.icons = false;\n"
+"       $.jstree.defaults.core.themes.dots = true;\n"
+"       $.jstree.defaults.contextmenu.show_at_node = false;\n"
+"       $.jstree.defaults.contextmenu.items = hubSearchTreeContextMenuHandler\n"
+"       treeDiv=$('.hubTdbTree');\n"
+"       treeDiv.jstree({\n"
+"               'conditionalselect' : function (node, event) { return false; },\n"
+"               'plugins' : ['conditionalselect', 'contextmenu']\n"
+"               });\n"
+"    }\n\n"
+"    return { init: init};\n\n"
+"}());\n"
+"\n"
+"$(function () {\n"
+"    hubSearchTree.init();\n"
+"});\n"
+);
+
 cartWebEnd();
 }
 
 char *excludeVars[] = {"Submit", "submit", "hc_one_url", 
     hgHubCheckUrl, hgHubDoClear, hgHubDoDisconnect,hgHubDoRedirect, hgHubDataText, 
     hgHubConnectRemakeTrackHub, NULL};
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 long enteredMainTime = clock1000();
 oldVars = hashNew(10);
 cgiSpoof(&argc, argv);
 cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars);
 cgiExitTime("hgHubConnect", enteredMainTime);
 return 0;
 }