Skip to content

Commit

Permalink
Add a Tabulator-based combined package search and list (#438)
Browse files Browse the repository at this point in the history
  • Loading branch information
rkent authored Oct 30, 2024
1 parent aec127a commit a74a701
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 7 deletions.
2 changes: 1 addition & 1 deletion _layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

<script type="text/javascript" src={{ "/js/jquery.js" | prepend: site.baseurl }}></script>
<script src={{ "/bootstrap/js/bootstrap.min.js" | prepend: site.baseurl }} type="text/javascript"></script>
<script src={{ "/js/jquery-cookie.js" | prepend: site.baseurl }} type="text/javascript"></script>
<script src={{ "/js/jquery-cookie.js" | prepend: site.baseurl }} type="text/javascript"></script>-
{% include_cached google_analytics.html %}
<script type="text/javascript" src={{ "/js/toc.js" | prepend: site.baseurl }}></script>

Expand Down
63 changes: 63 additions & 0 deletions _layouts/noscroll-default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width initial-scale=1" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<title>{% if page.title %}{{ page.title }}{% else %}{{ site.title }}{% endif %}</title>
<meta name="description" content="{{ site.description }}">

{% if page.canonical_url and page.canonical_url != page.url %}
<link rel="canonical" href="{{ page.canonical_url | replace: 'index.html', '' }}">
<meta http-equiv="refresh" content="0; url={{ page.canonical_url | replace: 'index.html', '' }}" />
<script>
window.location.href = "{{ page.canonical_url | replace: 'index.html','' }}";
</script>
{% else %}
<link rel="canonical" href="{{ page.url | replace: 'index.html', '' | prepend: site.baseurl | prepend: site.url }}">
{% endif %}

<link rel="icon" sizes="any" type="image/svg+xml" href="{{ site.baseurl }}/assets/rosindex_logo.svg">

{% if page.css_uris %}
{% for css_uri in page.css_uris %}
<link rel="stylesheet" href="{{ css_uri }}">
{% endfor %}
{% endif %}

<link rel="stylesheet" type="text/css" href="{{ site.baseurl}}/bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="{{ "/css/main.css" | prepend: site.baseurl }}">
{% comment %}<link rel="stylesheet" type="text/css" media="screen" href="{{ "/css/toc.css" | prepend: site.baseurl}}">{% endcomment %}

{% if page.script_uris %}
{% for script_uri in page.script_uris %}
<script type="text/javascript" src="{{ script_uri }}"></script>
{% endfor %}
{% endif %}

<script type="text/javascript" src={{ "/js/jquery.js" | prepend: site.baseurl }}></script>
<script src={{ "/bootstrap/js/bootstrap.min.js" | prepend: site.baseurl }} type="text/javascript"></script>
<script src={{ "/js/jquery-cookie.js" | prepend: site.baseurl }} type="text/javascript"></script>
{% include_cached google_analytics.html %}
<script type="text/javascript" src={{ "/js/toc.js" | prepend: site.baseurl }}></script>

<script src={{ "/js/distro_switch.js" | prepend: site.baseurl }}></script>
</head>

<body style="height: 100vh;">

{% include_cached header.html %}

<div class="page-content" style="height: var(--content-height);">
<div class="wrapper" style="height:100%;">
{{ content }}
</div>
</div>

{% include_cached footer.html %}

</body>

</html>
271 changes: 271 additions & 0 deletions _layouts/search_packages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
---
layout: noscroll-default
---
<div class="container-fluid" style="padding-top:10px; height:100%;">
<div class="container-fluid" style="height:100%;">
<div class="row" style="height:var(--distro-height);">
{% include distro_switch.html %}
</div>
<div class="row" style="height:var(--search-height);">
<div class="panel panel-default" style="height:100%;">
<div class="panel-heading" style="height:100%;">
<div class="row" style="height:100%;">
<div class="col-xs-6">
<div class="input-group">
<span class="input-group-addon">
<input type="checkbox" id="search-enable-box"/>
<span class="hidden-xs"> Enable search </span>
</span>
<input id="search-query" type="text" class="form-control"
autocomplete="off" placeholder="Search packages">
<span class="input-group-btn">
<button id="search-button" title="Search packages" class="btn btn-default">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
</div>
<div class="col-xs-6">
<p style="line-height:35px"><span class="hidden-xs">Showing </span>
<span id="table-filtered-count">0</span> of
<span id="table-unfiltered-count">0</span> packages
</p>
</div>
</div>
</div>
</div>
</div>
<div class="row" style="height:calc(100% - var(--search-height) - var(--distro-height))">
<div id="packages-table"></div>
</div>
</div>
</div>

<!-- See https://github.com/olifolkerd/tabulator/issues/4419 for issue with Tabulator post-5.6.0 -->
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/tabulator.min.css" rel="stylesheet">
<!-- <script src={{ "/js/tabulator.js" | prepend: site.baseurl }}></script> -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/tabulator.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js"></script>
<script src="{{site.baseurl}}/js/lunr.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
var gData_promises = {};
var gIndex_promises = {};
var gTable = null;
var gDistro = "{{ site.distros[0] }}";

function addFilter(table, field, type, value) {
// Add a new table filter, removing existing
removeFilterByField(table, field);
table.addFilter(field, type, value);
}

function removeFilterByField(table, field) {
// remove all filters on a specified field, if any
for (const existingFilter of table.getFilters()) {
if ('field' in existingFilter && existingFilter['field'] == field) {
table.removeFilter(existingFilter.field, existingFilter.type, existingFilter.value);
}
}
}

function getData(distro) {
// start and return a promise to get distro json data
if (distro in gData_promises) {
return gData_promises[distro];
}
gData_promises[distro] =
fetch(`{{site.baseurl}}/search/packages/data.${distro}.json`)
.then((response) => response.json());

return gData_promises[distro];
}

function getIndex(distro) {
console.log(`getIndex for ${distro}`);
// start and return a promise to get distro json index
if (distro in gIndex_promises) {
return gIndex_promises[distro];
}
gIndex_promises[distro] =
fetch(`{{site.baseurl}}/search/packages/index.${distro}.json`)
.then((response) => response.json())
.then((indexData) => {
return lunr.Index.load(indexData);
});

return gIndex_promises[distro];
}

function getWindowSearchQuery() {
return new URL(window.location.toString()).searchParams.get('pkgs');
}

// Populate the search input with querystring parameter
function setWindowSearchQuery(query) {
var url = new URL(window.location.toString());
url.searchParams.set('pkgs', query);
window.history.pushState({}, '', url);
}

function copySearchQueryToUI() {
var query = getWindowSearchQuery() || '';
$('#search-query').val(query);
$("#search-enable-box").prop("checked", !!query);
}

function copyUIToSearchQuery() {
if ($("#search-enable-box").prop("checked")) {
setWindowSearchQuery($('#search-query').val());
} else {
setWindowSearchQuery('');
}
}

// Returns a promise resolving to a search array of id hits.
function getSearchArray(distro, query) {
if (!query) {
return Promise.resolve(null);
}
return getIndex(distro).then(searchIndex => {
var searchResults = searchIndex.search(query);
searchArray = searchResults.map((result) => parseInt(result.ref));
console.log(`Search returned ${searchArray.length} results`);
// Tabulator filters don't seem to work with zero entries
searchArray = searchArray.length ? searchArray : [-1];
return searchArray;
});
}

function doSearch(table, distro, query) {
console.log(`doSearch query="${query}"`);
getSearchArray(distro, query)
.then(searchArray => {
if (searchArray) {
addFilter(table, 'id', 'in', searchArray)
} else {
removeFilterByField(table, 'id');
}
})
}

function makeTable(distro, data, searchArray) {
const CALENDAR = "\u{1F4C5}";
const STAR = "\u2605";
const LEFT_ARROW = "\u2B05"
const RIGHT_ARROW = "\u27A1"

var initialFilter = (searchArray === undefined || searchArray === null) ?
[] : [{field: 'id', type: 'in', value: searchArray}];

const table = new Tabulator("#packages-table", {
maxHeight:"100%",
// height: "400px",
data:data, //assign data to table
layout:"fitColumns", //fit columns to width of table (optional)
rowHeight:30, // Workaround for https://github.com/olifolkerd/tabulator/issues/4419
initialFilter: initialFilter,
columns:[ //Define Table Columns
{ title:"Name", field:"url", width:200,
formatter:"link", formatterParams:{labelField:"name", urlPrefix:"{{site.baseurl}}"}},
{ title:"Description", field:"description", minWidth:300},
{ title:STAR, field:"released", width:40, formatter:"tickCross", headerTooltip:"release status", hozAlign:"center",
formatterParams: {allowTruthy: true, allowEmpty: true}},
{ title:CALENDAR, field: "last_commit_time", width:80, headerHozAlign:"center", headerTooltip:"last commit date",
sorter:"date", sorterParams:{format:"yyyy-MM-dd"}},
{ title:LEFT_ARROW, field:"pkg_deps", width:40, headerTooltip:"package dependency count"},
{ title:RIGHT_ARROW, field:"dependants", width:40, headerTooltip:"package used by count"},
{ title:"Authors", field:"authors", width:120},
{ title:"Maintainers", field:"maintainers", width:120},
{ title:"Repo", field:"repo_name", width:100,
formatter:"link", formatterParams:{labelField:"repo_name", url:(cell) =>
{ return `{{site.baseurl}}/r/${cell.getValue()}/#${distro}`}}},
{ title:"Org", field:"org", width:100},
],
});
return table;
}

// Returns a promise that resolves to a Tabulator table
function showTable(distro, query) {
console.log(`showTable distro: ${distro}, query: ${query}`);
var searchPromise = query ? getSearchArray(distro, query) : Promise.resolve(null);
return Promise.all([getData(distro), searchPromise]).then(values => {
var data = values[0];
var searchArray = values[1];
const table = makeTable(distro, data, searchArray);
$("#table-filtered-count").text(data.length);
$("#table-unfiltered-count").text(data.length);
table.on("dataFiltered", (filters, rows) => {
$("#table-filtered-count").text(rows.length);
$("#table-unfiltered-count").text(table.getDataCount());
});
table.on("tableBuilt", () => {
$("#table-filtered-count").text(table.getDataCount('active'));
$("#table-unfiltered-count").text(table.getDataCount());
});
return table;
});
}

$(function() {
console.log('document ready');
copySearchQueryToUI();
gDistro = setupDistroSwitch("{{ site.distros[0] }}");
var query = getWindowSearchQuery();
showTable(gDistro, query)
.then(table => gTable = table);

window.addEventListener('hashchange', (event) => {
// Reload the table if the distro changes
var url = new URL(document.location.toString());
console.log(`hash change url: ${url.href}`);
if (url.hash) {
gDistro = url.hash.substring(1);
// TODO: change cookie?
} else {
gDistro = "{{ site.distros[0] }}";
}
console.log("overriding in search_packages via anchor "+gDistro+" distro");
var query = getWindowSearchQuery();
showTable(gDistro, query)
.then(table => gTable = table);
});

$('#search-query').on('focus', (e) => {
// Pre-load index to speed up search response
getIndex(gDistro);
});

$('#search-query').bind('keypress', (e) => {
if (e.which == 13) {
$("#search-enable-box").prop("checked", true);
copyUIToSearchQuery();
var query = getWindowSearchQuery() || '';
doSearch(gTable, gDistro, query)
}
});

$("#search-enable-box").on('click', (e) => {
element = e.currentTarget;
console.log(`#search-enable click ${element.checked}`);
query = $("#search-query").val();
if (element.checked && query) {
copyUIToSearchQuery();
doSearch(gTable, gDistro, query);
} else {
setWindowSearchQuery('');
removeFilterByField(gTable, 'id');
}
});

$("#search-button").on('click', (e) => {
query = $("#search-query").val();
if (query) {
$("#search-enable-box").prop("checked", true);
copyUIToSearchQuery();
doSearch(gTable, gDistro, query);
}
});

});
</script>
18 changes: 14 additions & 4 deletions _plugins/rosindex_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,10 @@ def load_rosdeps(rosdistro_path, platforms, package_manager_names)
return rosdep_data
end

def generate_search_package_list(site, elements, default_sort_key)
site.pages << SearchPackageListPage.new(site, default_sort_key, 1, 1, elements, true)
end

def generate_sorted_paginated_deps(site, elements_sorted, default_sort_key, n_elements, elements_per_page, page_class)

n_pages = (n_elements / elements_per_page).floor + 1
Expand Down Expand Up @@ -1507,6 +1511,9 @@ def generate(site)
packages_sorted = sort_packages(site)
generate_sorted_paginated(site, packages_sorted, 'time', @package_names.length, site.config['packages_per_page'], PackageListPage)

search_packages_sorted = sort_packages(site)
generate_search_package_list(site, search_packages_sorted, 'time')

# create rosdep pages
puts ("Generating rosdep pages...").blue

Expand Down Expand Up @@ -1562,11 +1569,14 @@ def generate(site)
'unreleased' => if repo_snapshot.released then 'is:unreleased' else '' end,
'version' => p['version'],
'description' => p['description'].strip,
'maintainers' => p['maintainers'] * " ",
'authors' => p['authors'] * " ",
'maintainers' => p['maintainers'] * ", ",
'authors' => p['authors'] * ", ",
'distro' => distro,
'instance' => repo.id,
'readme' => readme_filtered
'pkg_deps' => p['pkg_deps'].length,
'dependants' => p['dependants'].length,
'readme' => readme_filtered,
'org' => URI(repo.uri).path.split('/')[1]
}

dputs 'indexed: ' << "#{package_name} #{instance_id} #{distro}"
Expand All @@ -1579,7 +1589,7 @@ def generate(site)
indexed_fields = [
'baseurl', 'instance', 'url', 'tags:100','name:100',
'version', 'description:50', 'maintainers', 'authors',
'distro','readme', 'released', 'unreleased'
'distro','readme', 'released', 'unreleased', 'org'
]
site.static_files.push(*precompile_lunr_index(
site, packages_index, reference_field, indexed_fields,
Expand Down
Loading

0 comments on commit a74a701

Please sign in to comment.