-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Created a simple search page as a proof-of-concept. (#4)
This requires addition of CORS support for the /query endpoint.
- Loading branch information
Showing
6 changed files
with
471 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
on: | ||
push: | ||
branches: | ||
- master | ||
|
||
name: Demonstration search | ||
|
||
jobs: | ||
search: | ||
name: Publish demonstration search | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- name: Deploy to GitHub pages 🚀 | ||
uses: JamesIves/github-pages-deploy-action@v4 | ||
with: | ||
clean: false | ||
branch: gh-pages | ||
folder: html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,304 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="description" content="SewerRat search"> | ||
<meta name="keywords" content="SeweRat, search"> | ||
<meta name="author" content="Aaron Lun"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
|
||
<title>SewerRat search</title> | ||
|
||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/renderjson@1.4.0/renderjson.min.js"></script> | ||
<style> | ||
.renderjson { | ||
margin: 0.1em 0px; | ||
} | ||
|
||
.renderjson a { | ||
text-decoration: none; | ||
color: grey; | ||
} | ||
.renderjson .disclosure { | ||
color: grey; | ||
font-size: 150%; | ||
} | ||
</style> | ||
|
||
<script type="text/javascript" src="parseQuery.js"></script> | ||
<script type="text/javascript"> | ||
var query_body = null; | ||
|
||
function populateQueryBody() { | ||
const all_clauses = []; | ||
|
||
const errdiv = self.document.getElementById("parse-error"); | ||
errdiv.replaceChildren(); | ||
|
||
const metadiv = self.document.getElementById('metadata'); | ||
try { | ||
let parsed = parseQuery(metadiv.value); | ||
all_clauses.push(parsed.metadata); | ||
} catch (e) { | ||
errdiv.appendChild(self.document.createTextNode(e.message)); | ||
return false; | ||
} | ||
|
||
const user = self.document.getElementById('user'); | ||
if (user.value != "") { | ||
all_clauses.push({ "type": "user", "user": user.value }); | ||
} | ||
|
||
const path = self.document.getElementById('path'); | ||
if (path.value != "") { | ||
all_clauses.push({ "type": "path", "path": path.value }); | ||
} | ||
|
||
const date_from = self.document.getElementById('date-from'); | ||
if (date_from.valueAsDate != null) { | ||
all_clauses.push({ "type": "time", "time": date_from.valueAsDate.getTime() / 1000, "after": true }); | ||
} | ||
|
||
const date_to = self.document.getElementById('date-to'); | ||
if (date_to.valueAsDate != null) { | ||
all_clauses.push({ "type": "time", "time": date_to.valueAsDate.getTime() / 1000 }); | ||
} | ||
|
||
let query; | ||
if (all_clauses.length == 1) { | ||
query = all_clauses[0]; | ||
} else { | ||
query = { "type": "and", "children": all_clauses }; | ||
} | ||
|
||
query_body = JSON.stringify(query); | ||
return; | ||
} | ||
|
||
const base_url = "http://0.0.0.0:8080"; | ||
var query_next = null; | ||
var sofar = 0; | ||
|
||
async function populateSearchResults(request_endpoint, clear_existing) { | ||
let resp = await fetch(base_url + request_endpoint, { | ||
method: "POST", | ||
body: query_body, | ||
headers: { | ||
"Content-Type": "application/json" | ||
} | ||
}) | ||
|
||
const resdiv = self.document.getElementById('search'); | ||
if (clear_existing) { | ||
resdiv.replaceChildren(); | ||
sofar = 0; | ||
} | ||
function populateResultError(message) { | ||
let errmsg = self.document.createElement("p"); | ||
errmsg.style["color"] = "red"; | ||
errmsg.textContent = "Oops, looks like something went wrong (" + message + ")"; | ||
resdiv.appendChild(errmsg); | ||
return; | ||
} | ||
|
||
if (!resp.ok) { | ||
let message = String(resp.status) + " " + resp.statusText; | ||
try { | ||
let body = await resp.json(); | ||
if ("reason" in body) { | ||
message += ": " + body.reason; | ||
} | ||
} catch (e) {} | ||
populateResultError(message); | ||
return; | ||
} | ||
|
||
let output; | ||
try { | ||
output = await resp.json(); | ||
} catch (e) { | ||
populateResultError(e.message); | ||
return; | ||
} | ||
|
||
renderjson.set_icons("⊕", "⊖"); | ||
for (var i = 0; i < output.results.length; ++i) { | ||
const x = output.results[i]; | ||
const child = self.document.createElement("div"); | ||
|
||
let counter = self.document.createElement("span"); | ||
let global_i = sofar + i + 1; | ||
var col, bg; | ||
if (global_i % 2 === 1) { | ||
col = "black"; | ||
bg = "lightgrey"; | ||
} else { | ||
col = "white"; | ||
bg = "#4D4D4D"; | ||
} | ||
counter.textContent = String(global_i); | ||
counter.style = "display: inline-block; min-width: 2em; background-color: " + bg + "; color: " + col + "; margin-right: 5px; padding: 5px; text-align: center"; | ||
child.appendChild(counter); | ||
|
||
let pchild = self.document.createElement("code"); | ||
pchild.textContent = x.path; | ||
child.appendChild(pchild); | ||
child.append(self.document.createTextNode(", created by ")); | ||
let uchild = self.document.createElement("code"); | ||
uchild.textContent = x.user; | ||
child.appendChild(uchild); | ||
child.append(self.document.createTextNode(" at " + (new Date(x.time * 1000)).toString())); | ||
|
||
child.appendChild(renderjson(x.metadata)); | ||
resdiv.appendChild(child); | ||
resdiv.appendChild(self.document.createElement("hr")); | ||
} | ||
|
||
const nextdiv = self.document.getElementById('search-next'); | ||
if ("next" in output) { | ||
query_next = output.next; | ||
sofar += output.results.length; | ||
|
||
let newform = document.createElement("form"); | ||
newform.onsubmit = continueSearch; | ||
let newbutton = document.createElement("input"); | ||
newbutton.type = "submit"; | ||
newbutton.value = "More results"; | ||
newform.appendChild(newbutton); | ||
nextdiv.appendChild(newform); | ||
} else { | ||
query_next = null; | ||
nextdiv.replaceChildren(); | ||
} | ||
return; | ||
} | ||
|
||
function startNewSearch() { | ||
populateQueryBody(); | ||
populateSearchResults("/query", true) | ||
return false; | ||
} | ||
|
||
function continueSearch() { | ||
populateSearchResults(query_next, false) | ||
return false; | ||
} | ||
</script> | ||
|
||
<style> | ||
.tooltip { | ||
position: relative; | ||
display: inline-block; | ||
border-bottom: 1px dotted black; | ||
} | ||
|
||
.tooltip .tooltiptext { | ||
visibility: hidden; | ||
background-color: lightgrey; | ||
width: max-content; | ||
max-width: 50vw; /* (as much as you want) */ | ||
border-radius: 6px; | ||
padding: 5px 5px; | ||
|
||
/* Position the tooltip */ | ||
position: absolute; | ||
z-index: 1; | ||
} | ||
|
||
.tooltip:hover .tooltiptext { | ||
visibility: visible; | ||
} | ||
</style> | ||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> | ||
</head> | ||
|
||
<body> | ||
<h1>SewerRat search</h1> | ||
|
||
<form onsubmit="return startNewSearch();"> | ||
<div style="display: flex; align-items: center; margin-bottom: 0.8em"> | ||
<label for="metadata" style="margin-right: 0.2em">Metadata:</label> | ||
<textarea id="metadata" name="metadata" rows="1" style="width:40vw"></textarea> | ||
<div class="tooltip" style="margin-left: 0.2em"> | ||
<i class="fa fa-question-circle" aria-hidden="true"></i> | ||
<span class="tooltiptext"> | ||
Free text search on the metadata. Leave empty to skip this filter. | ||
For simple use cases, just enter one or more search terms, and we'll search for metadata files that match all the terms. | ||
<br><br> | ||
Advanced users can use the <code>AND</code> and <code>OR</code> keywords to assemble complex queries. | ||
(Make sure to use all-caps for these keywords.) | ||
This can be combined with parentheses to control precedence, e.g., <code>(a b OR c d) AND (e f)</code>; | ||
otherwise, <code>AND</code> takes precedence over <code>OR</code>. | ||
Note that any sequence of adjacent search terms are implicitly <code>AND</code>, | ||
i.e., the query above can be expanded as <code>((a AND b) OR (c AND d)) AND (e AND f))</code>. | ||
<br><br> | ||
Even more advanced users can prefix any sequence of search terms with the name of a metadata field, | ||
to only search for matches within that field of the metadata file, e.g., | ||
<code>(title: prostate cancer) AND (genome: GRCh38 OR genome: GRCm38)</code>. | ||
Note that this does not extend to the <code>AND</code> and <code>OR</code> keywords, | ||
i.e., <code>title:foo OR bar</code> will not limit the search for <code>bar</code> to the <code>title</code> field. | ||
<br><br> | ||
Extremely advanced users can attach a <code>%</code> wildcard to any term to enable a partial search, | ||
e.g., <code>neur%</code> will match files with <code>neuron</code>, <code>neural</code>, <code>neurological</code>, etc. | ||
</span> | ||
</div> | ||
<div id="parse-error" style="color:red; margin-left:0.2em"></div> | ||
</div> | ||
|
||
<label for="user">User:</label> | ||
<input type="text" id="user" name="user" placeholder="user123"> | ||
<div class="tooltip"> | ||
<i class="fa fa-question-circle" aria-hidden="true"></i> | ||
<span class="tooltiptext"> | ||
Unix ID of the user who created the file. | ||
Leave empty to skip this filter. | ||
</span> | ||
</div> | ||
<br><br> | ||
|
||
<label for="path">Path:</label> | ||
<input type="text" id="path" name="path" placeholder="/some/location/on/the/file-system"> | ||
<div class="tooltip"> | ||
<i class="fa fa-question-circle" aria-hidden="true"></i> | ||
<span class="tooltiptext"> | ||
Any substring of the absolute path to the file, e.g., | ||
<code>/home/user/user1234/foo/bar</code>, | ||
<code>user1234/foo</code>, | ||
<code>foo/bar</code>, | ||
<code>1234/foo/b</code>. | ||
Leave empty to skip this filter. | ||
</span> | ||
</div> | ||
<br><br> | ||
|
||
<label for="date-from">Date (from):</label> | ||
<input type="date" id="date-from" name="date-from"> | ||
<div class="tooltip"> | ||
<i class="fa fa-question-circle" aria-hidden="true"></i> | ||
<span class="tooltiptext"> | ||
Only files with modification times after this date are considered. | ||
Leave empty to skip this filter. | ||
</span> | ||
</div> | ||
<br><br> | ||
|
||
<label for="date-to">Date (to):</label> | ||
<input type="date" id="date-to" name="date-to"> | ||
<div class="tooltip"> | ||
<i class="fa fa-question-circle" aria-hidden="true"></i> | ||
<span class="tooltiptext"> | ||
Only files with modification times before this date are considered. | ||
Leave empty to skip this filter. | ||
</span> | ||
</div> | ||
<br><br> | ||
|
||
<input type="submit" value="Search"> | ||
</form> | ||
<hr style="border-width:3px;"> | ||
|
||
<div id="search"></div> | ||
<div id="search-next"></div> | ||
|
||
</body> | ||
</html> |
Oops, something went wrong.