catalog, query UI: fix race condition and stabilize UI
add autocomplete, sidebar, deselect, and paging to UI
stability improvements to catalog
Change-Id: I2e873a41eaa8c10d9f9a1d605f33fb5217112d64
refs: #2634, 2638
diff --git a/client/query/query.html b/client/query/query.html
index b8f0e61..9e68518 100644
--- a/client/query/query.html
+++ b/client/query/query.html
@@ -6,122 +6,258 @@
<meta charset="UTF-8" />
- <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
- <script src="http://code.jquery.com/ui/1.10.1/jquery-ui.js"></script>
-
+ <script type="text/javascript" src="../jquery/jquery-latest.min.js"></script>
+ <script type="text/javascript" src="../jquery/ui/1.10.1/jquery-ui.js"></script>
+ <script type="text/javascript" src="../ndn-js/build/ndn.js"></script>
<link rel="stylesheet" href="query.css">
+ <link rel="stylesheet" href="../jquery/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script>
- var searchMenuOptions = {};
- var results = {};
+ // {@ @todo: this need to be configured before the document load
+ var catalog = "/catalog/myUniqueName";
+ var face = new Face({
+ host: "localhost",
+ port: 9696
+ });
+
+ // @}
+
+ var searchMenuOptions = {}
+ var results = [];
var resultCount = 0;
var page = 1;
var totalPages = 1;
+ var selectedSearch = {};
+ var dropdown = [];
$(document).ready(function () {
var searchMenu = $(".cssmenu");
var currentPage = $(".page");
var resultTable = $(".resultTable");
- var data = $.getJSON("sample.json", function () {
- })
- .done(function( data ) {
+ var data = $.getJSON("search_catagories.json", function () {}).done(function (data) {
$.each(data, function (pageSection, contents) {
if (pageSection == "SearchCatagories") {
-
$.each(contents, function (search, searchOptions) {
search = search.replace(/\_/g, " ");
searchMenu.append('<li id="' + search + '" onclick="getDropDown(this.id)"><a href="#">' + search + '</a></li>');
- searchMenuOptions[search] = searchOptions;
+ searchMenuOptions[String(search)] = searchOptions;
});
- } else if (pageSection == "QueryResults") {
- results = {};
- resultCount = 0;
- var view = [0, 0];
- $.each(contents, function (queryResult, field) {
- if (queryResult == "Length") {
- resultCount = field;
- } else if (queryResult == "View") {
- view = field;
- } else if (queryResult == "Results") {
- resultTable.empty();
- resultTable.append('<tr><td>Results</td></tr>');
-
- $.each(field, function (entryCount, fullResult) {
-
- $.each(fullResult, function (name, metadata) {
- resultTable.append('<tr><td onclick="getDetails(this.id)" id="' + name + '">' + name + '</td></tr>');
- results[name] = metadata;
- });
-
- });
- } else {
- console.error("Unknown field " + queryResult);
- }
- });
-
- // Calculating the current page and the view
- var diff = view[1] - view[0] + 1;
- if (diff > 0) {
- totalPages = Math.ceil(resultCount / diff);
- page = Math.ceil(view[0] / diff) + 1;
- } else {
- totalPages = 1;
- page = 1;
- }
-
- if (page != 1) {
- console.log(view[0]);
- currentPage.append('<span id="' + (view[0] - 1) + '" onclick="getPage(this.id)"><a href="#"><</a>');
- }
-
- // This section of code creates the paging for the results.
- // To prevent it from having a 1000+ pages, it will only show the 5 pages before/after
- // the current page and the total pages (expect users not to really jump around a lot).
- for (var i = 0; i < totalPages; ++i) {
- if (i + 1 == 1 || i + 1 == page || i + 1 == totalPages || (i + 1 < page && i + 4 > page) || (i + 1 > page && i - 4 < page)) { // in our current page range
- currentPage.append(' ');
- currentPage.append('<span id="' + (i + 1) + '" onclick="getPage(this.id)">');
- if (i + 1 != page) {
- currentPage.append('<a href="#">' + (i + 1) + '</a>')
- } else {
- currentPage.append(i + 1);
- }
- currentPage.append('</span>');
- } else { // Need to skip ahead
- currentPage.append(' ...');
- if (i == page + 5) {
- i = totalPages - 2;
- } else if (i < page - 7) {
- i = page - 6;
- }
- }
- }
- if (page != totalPages) {
- currentPage.append(' <span id="' + (page + diff) + '" onclick="getPage(this.id)"><a href="#">></a>');
- }
}
});
- })();
+ });
});
- var state = "";
+ function onData(data) {
+ var payloadStr = data.content.toString().split("\n")[0];
+
+ var queryResults = JSON.parse(payloadStr);
+
+ var resultTable = $(".resultTable");
+ $.each(queryResults, function (queryResult, field) {
+
+ if (queryResult == "next") {
+ populateAutocomplete(field);
+ }
+
+ $.each(field, function (entryCount, name) {
+ results.push(name);
+ });
+ });
+
+ // Calculating the current page and the view
+ totalPages = Math.ceil(resultCount / 20);
+ populateResults(0);
+ }
+
+ var state = {};
+
+ function query(prefix, parameters, callback, pipeline) {
+ results = [];
+ dropdown = [];
+
+ var resultTable = $(".resultTable");
+ resultTable.empty();
+ resultTable.append('<tr><td>Results</td></tr>');
+
+ var queryPrefix = new Name(prefix);
+ queryPrefix.append("query");
+
+ queryPrefix.append(JSON.stringify(parameters));
+
+ state = {
+ prefix: new Name(prefix),
+ userOnData: callback,
+ outstanding: {},
+ nextSegment: 0,
+ };
+
+ /*if (state.hasOwnProperty("version")) {
+ console.log("state already has version");
+ }*/
+
+ var queryInterest = new Interest(queryPrefix);
+ queryInterest.setInterestLifetimeMilliseconds(10000);
+
+ face.expressInterest(queryInterest,
+ onQueryData,
+ onQueryTimeout);
+
+ state["outstanding"][queryInterest.getName().toUri()] = 0;
+ }
+
+ function expressNextInterest() {
+ // @todo pipelines
+ var nextName = new Name(state["results"]);
+ nextName.appendSegment(state["nextSegment"]);
+
+ var nextInterest = new Interest(nextName);
+ nextInterest.setInterestLifetimeMilliseconds(10000);
+
+ face.expressInterest(nextInterest,
+ onQueryResultsData,
+ onQueryResultsTimeout);
+
+ state["nextSegment"] ++;
+ state["outstanding"][nextName.toUri()] = 0;
+ }
+
+ function onQueryData(interest, data) {
+ var name = data.getName();
+
+ delete state["outstanding"][interest.getName().toUri()];
+
+ state["version"] = name.get(state["prefix"].size() + 2).toVersion();
+
+ state["results"] = new Name(state["prefix"]).append("query-results").appendVersion(state["version"]);
+
+ expressNextInterest();
+ }
+
+ function onQueryResultsData(interest, data) {
+ var name = data.getName();
+ delete state["outstanding"][interest.getName().toUri()];
+
+ if (!name.get(-1).equals(new Name.Component("END"))) {
+ expressNextInterest();
+ } else {
+ alert("found final block");
+ }
+
+ state["userOnData"](data);
+ }
+
+ function onQueryTimeout(interest) {
+ var uri = interest.getName().toUri();
+ if (state["outstanding"][uri] < 1) {
+ state["outstanding"][uri] ++;
+ face.expressInterest(interest,
+ onQueryData,
+ onQueryTimeout);
+ } else {
+ delete state["outstanding"][uri];
+
+ // We modify the autocomplete box here because we need to know
+ // we have all of the entries first. Fairly hacky.
+ var autocompleteFullName = autocompleteText.value;
+ for (var i = 0; i < dropdown.length; ++i) {
+ if (dropdown[i].substr(0, dropdown[i].length - 1).toUpperCase === autocompleteText.value.toUpperCase || dropdown.length == 1) {
+ autocompleteText.value = dropdown[i];
+ }
+ }
+ }
+ }
+
+ function onQueryResultsTimeout(interest) {
+ var uri = interest.getName().toUri();
+ if (state["outstanding"][uri] < 1) {
+ state["outstanding"][uri] ++;
+ face.expressInterest(interest,
+ onQueryResultsData,
+ onQueryResultsTimeout);
+ } else {
+ delete state["outstanding"][uri];
+ // We modify the autocomplete box here because we need to know
+ // we have all of the entries first. Fairly hacky.
+ var autocompleteFullName = autocompleteText.value;
+ for (var i = 0; i < dropdown.length; ++i) {
+ if (dropdown[i].substr(0, dropdown[i].length - 1).toUpperCase === autocompleteText.value.toUpperCase || dropdown.length == 1) {
+ autocompleteText.value = dropdown[i];
+ }
+ }
+ }
+ }
+
+
+ var currentViewIndex = 0;
+
+ function populateResults(startIndex) {
+ var resultTable = $(".resultTable");
+ resultTable.empty();
+ resultTable.append('<tr><td>Results</td></tr>');
+
+
+ for (var i = startIndex; i < startIndex + 20 && i < results.length; ++i) {
+ resultTable.append('<tr><td>' + results[i] + '</td></tr>');
+ }
+
+ if (results.length <= 20) {
+ page = 1;
+ } else {
+ page = startIndex / 20 + 1;
+ }
+
+ totalPages = Math.ceil(results.length / 20);
+
+ var currentPage = $(".page");
+ currentPage.empty();
+ if (page != 1) {
+ currentPage.append('<a href="#" onclick="getPage(this.id);" id="<"><</a>');
+ }
+ // This section of code creates the paging for the results.
+ // To prevent it from having a 1000+ pages, it will only show the 5 pages before/after
+ // the current page and the total pages (expect users not to really jump around a lot).
+ for (var i = 1; i <= totalPages; ++i) {
+ if (i == 1 || i == totalPages // Min or max
+ || (i <= page && i + 5 >= page) // in our current page range
+ || (i >= page && i - 5 <= page)) { // in our current page range
+ if (i != page) {
+ currentPage.append(' <a href="#" onclick="getPage(' + i + ');">' + i + '</a>')
+ if (i == 1 && page > i + 5) {
+ currentPage.append(' ... ');
+ }
+ } else {
+ currentPage.append(' ' + i);
+ }
+ } else { // Need to skip ahead
+ if (i == page + 6) {
+ currentPage.append(' ... ');
+
+ currentPage.append(' <a href="#" onclick="getPage(this.id);" id=">">></a>')
+ i = totalPages - 1;
+ }
+ }
+ }
+ currentPage.append(' ' + results.length + ' results');
+ }
+
+ var dropState = "";
function getDropDown(str) {
var searchMenu = $(".cssmenu");
- if (str == state) {
- state = "";
+ if (str == dropState) {
+ dropState = "";
searchMenu.find("#" + str).find("#options_" + str).empty();
} else {
- state = str;
+ dropState = str;
$.each(searchMenuOptions, function (search, fields) {
if (search === str) {
searchMenu.find("#" + search).append('<ul id="options_' + search + '" class="sub-menu">');
for (var i = 0; i < fields.length; ++i) {
- searchMenu.find("#options_" + search).append('<li><a href="#">' + fields[i] + '</a></li>');
+ searchMenu.find("#options_" + search).append('<li id="' + fields[i] + '"onclick="submitCatalogSearch(this.id)"><a href="#">' + fields[i] + '</a></li>');
}
searchMenu.append('</ul>');
} else {
@@ -133,45 +269,143 @@
}
}
- function getPage(str) {
- // @todo
- }
+ function getPage(clickedPage) {
+ console.log(clickedPage);
- function getDetails(str) {
- // @todo: Identify the correct way to integrate the download backend so that we know who
- // we should be pointing at.
+ var nextPage = clickedPage;
+ if (clickedPage === "<") {
+ nextPage = page - 5;
+ } else if (clickedPage === ">") {
+ console.log("> enabled");
- var details = "<h2>" + str + "<h2>";
-
- details += '<form action="getDownload()"><input type="submit" value="Download"></form>';
- details += "<table>";
- $.each(results[str], function (fieldName, field) {
- console.log(fieldName);
-
- details += "<tr>"
- details += "<td>" + fieldName + "</td>";
- if (typeof field === 'object') {
- details += "<td><table>";
- $.each(field, function (name, fields) {
- details += '<tr><td>' + name + '</td><td>' + fields + '<td></tr>';
- });
- details += "</table></td>";
- } else {
- details += "<td>" + field + "</td>"
- }
- details += "</tr>"
-
- });
- details += "</table>";
- details += '<form action="getDownload()"><input type="submit" value="Download"></form>';
-
- var detailsWindow = window.open("", str, "width=400, height=600, toolbar=no, menubar=no");
- detailsWindow.document.write(details);
- detailsWindow.onload = function () { // of course you can use other onload-techniques
- jQuery(detailsWindow.document.head).append('<script>function getDownload() {alert("Yay";)}</' + "script>"); // or any other dom manipulation
+ nextPage = page + 5;
}
- //window.open("details.html", "_blank", "toolbar=no, scrollbars=yes, resizable=yes, top=500, left=500, width=400, height=400");*/
+ nextPage--; // Need to adjust for starting at 0
+
+ if (nextPage < 0 ) {
+ nextPage = 0;
+ console.log("0 enabled");
+ } else if (nextPage > totalPages - 1) {
+ nextPage = totalPages - 1;
+ console.log("total enabled");
+ }
+
+ populateResults(nextPage * 20);
+ return false;
+ }
+
+ function submitAutoComplete() {
+ if (autocompleteText.value.length > 0) {
+ var selection = autocompleteText.value;
+ $.each(dropdown, function (i, dropdownEntry) {
+ if (dropdownEntry.substr(0, dropdownEntry.length - 1) == selection) {
+ selection = dropdownEntry;
+ }
+ });
+
+ selectedSearch["?"] = selection;
+ query(catalog, selectedSearch, onData, 1);
+ delete selectedSearch["?"];
+ }
+ }
+
+ function submitCatalogSearch(field) {
+ console.log("Sumbit Catalog Search: " + field);
+ // @todo: this logic isn't quite right
+ var remove = false;
+ $.each(selectedSearch, function (search, f) {
+ if (field == f) {
+ delete selectedSearch[field];
+ remove = true;
+ }
+ });
+ if (!remove) {
+ $.each(searchMenuOptions, function (search, fields) {
+ $.each(fields, function (index, f) {
+ if (f == field) {
+ selectedSearch[search] = field;
+ }
+ });
+ });
+ }
+ query(catalog, selectedSearch, onData, 1);
+ populateCurrentSelections();
+ return false;
+ }
+
+ function populateAutocomplete(fields) {
+ var isAutocompleteFullName = (autocompleteText.value.charAt(autocompleteText.value.length - 1) === "/");
+ var autocompleteFullName = autocompleteText.value;
+ for (var i = 0; i < fields.length; ++i) {
+ var fieldFullName = fields[i];
+ var entry = autocompleteFullName;
+ var skipahead = "";
+
+ if (isAutocompleteFullName) {
+ skipahead = fieldFullName.substr(autocompleteText.value.length, fieldFullName.length);
+ } else {
+ if (fieldFullName.charAt(autocompleteText.value.length) === "/") {
+ entry += "/";
+ skipahead = fieldFullName.substr(autocompleteText.value.length + 1, fieldFullName.length);
+ } else {
+ skipahead = fieldFullName.substr(autocompleteText.value.length, fieldFullName.length);
+ }
+ }
+ if (skipahead.indexOf("/") != -1) {
+ entry += skipahead.substr(0, skipahead.indexOf("/") + 1);
+ } else {
+ entry += skipahead;
+ }
+
+ var added = false;
+ for (var j = 0; j < dropdown.length && !added; ++j) {
+ if (dropdown[j] === entry) {
+ added = true;
+ } else if (dropdown[j] > entry) {
+ dropdown.splice(j, 0, entry);
+ added = true;
+ }
+ }
+ if (!added) {
+ dropdown.push(entry);
+ }
+
+ }
+ $("#autocompleteText").autocomplete({
+ source: dropdown
+ });
+ }
+
+ function populateCurrentSelections() {
+ var currentSelection = $(".currentSelections");
+ currentSelection.empty();
+
+ currentSelection.append("<p>Filtering on:");
+
+ $.each(selectedSearch, function (searchMenuCatagory, selection) {
+ currentSelection.append(' <a href="#" onclick="removeFilter(this.id);" id="' + searchMenuCatagory + ':' + selection + '">[X] ' + searchMenuCatagory + ":" + selection + '</a>');
+ });
+
+ currentSelection.append("</p>");
+ }
+
+
+ function removeFilter(filter) {
+ console.log("Remove filter" + filter);
+ var searchFilter = filter.split(":");
+
+ var search = "";
+ for (var j = 0; j < searchFilter.length; ++j) {
+ search += searchFilter[j] + " ";
+ }
+ console.log("Split values: '" + search + "'");
+
+ delete selectedSearch[searchFilter[0]];
+ query(catalog, selectedSearch, onData, 1);
+ populateCurrentSelections();
+
+ return false;
}
</script>
</head>
@@ -185,18 +419,25 @@
</ul>
+ <div class="currentSelections"></div>
+
+
<div class="autocomplete">
- <form action="submit()">
- <input type="text" name="autocomplete" class="textbox" id="autocomplete" placeholder="cmap" width="800px">
- <input type="submit" value="Search">
- </form>
+ <div class="ui-widget">
+ <label for="tags"> Search: </label>
+ <input id="autocompleteText" placeholder="/cmip" class="textbox" onkeydown="if (event.keyCode == 13) submitAutoComplete(); ">
+ <button id="autoButton" value="Search" onclick="submitAutoComplete()" id="autocompleteButton">Search</button>
+ </div>
+
+ <div class="page"></div>
+
+ <table class="resultTable">
+
+
+ </table>
</div>
- <table class="resultTable">
- </table>
- <div class="page"></div>
-
</body>
</html>