Skip to content

Commit

Permalink
Merge branch 'master' into formatting
Browse files Browse the repository at this point in the history
* master:
  Don't ignore `db` when building Docker image (ad-freiburg#101)
  Update deprecated setting for static storage (ad-freiburg#95)
  Build and push Docker image after each commit to the master (ad-freiburg#98)
  Update db/qleverui.sqlite3 to latest configs and example queries
  Display details for operation in "Analysis" tree as tooltip (ad-freiburg#80)
  Bump dependencies `requests`, `gunicorn`, `django` to latest version
  Improvements in Docker setup (ad-freiburg#90)
  Add keyword search to filter example queries (ad-freiburg#83)
  Restore default qleverui.sqlite3
  Fix regex in replacePredicatesList
  No subject AC query for short prefix
  • Loading branch information
IoannisNezis committed Sep 1, 2024
2 parents eb9c778 + 4fd05b7 commit 3c0f234
Show file tree
Hide file tree
Showing 15 changed files with 311 additions and 48 deletions.
Binary file removed .DS_Store
Binary file not shown.
13 changes: 2 additions & 11 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
# *
# !backend
# !qlever
# !manage.py
db
!db/master.sqlite3
!tests
!requirements.txt
!LICENSE
!README.md
!db
.git
!.git/HEAD
!.git/refs/heads
!venv
63 changes: 63 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Docker build and publish

on:
push:
branches: [ master ]

concurrency:
# When this is not a pull request, then we want all the docker containers to be pushed, we therefore
# directly fall back to the commit hash which will be distinct for each push to master.
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.sha}}'
cancel-in-progress: true

jobs:
docker:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get short sha
id: sha
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Get PR number
id: pr
run: echo "pr_num=$(git log --format=%s -n 1 | sed -nr 's/.*\(\#([0-9]+)\)/\1/p')" >> $GITHUB_OUTPUT

- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
env:
# We build multiplatform images which have an image index above the
# image manifests. Attach the annotations directly to the image index.
DOCKER_METADATA_ANNOTATIONS_LEVELS: "index"

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64

# Push to dockerhub, reuse the cached steps from the previous build.
push: true

# If this is a push on master, publish with short commit sha
# else use the ref name, which has to be the tag in this case.
# We have to explicitly add the "qlever:latest" tag for it to work correctly,
# see e.g. https://stackoverflow.com/questions/27643017/do-i-need-to-manually-tag-latest-when-pushing-to-docker-public-repository
tags: >
adfreiburg/qlever-ui:latest,
# Set Annotations and Labels that conform to the OpenContainers
# Annotations Spec
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.meta.outputs.labels }}

21 changes: 6 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
FROM docker.io/python:3.12.2-alpine3.19

LABEL "org.opencontainers.image.url"="https://github.com/ad-freiburg/qlever-ui"
LABEL "org.opencontainers.image.documentation"="https://github.com/ad-freiburg/qlever-ui"
LABEL "org.opencontainers.image.source"="https://github.com/ad-freiburg/qlever-ui"
LABEL "org.opencontainers.image.licenses"="Apache-2.0"
LABEL "org.opencontainers.image.title"="QLever UI"
LABEL "org.opencontainers.image.description"="A user interface for QLever"
LABEL "org.opencontainers.image.base"="docker.io/python:3.10.2-alpine3.15"
FROM index.docker.io/library/python:3.12.4-alpine3.20

ADD requirements.txt /app/requirements.txt

Expand All @@ -24,17 +16,16 @@ RUN set -ex \
&& apk add bash bash-completion make sqlite

COPY . /app
# ADD . /app
WORKDIR /app

ENV VIRTUAL_ENV /env
ENV PATH /env/bin:$PATH
ENV PYTHONUNBUFFERED 1
ENV VIRTUAL_ENV="/env"
ENV PATH="/env/bin:${PATH}"
ENV PYTHONUNBUFFERED="1"

# collect static resources
# Collect static resources
RUN ./manage.py collectstatic

CMD ["gunicorn", "--bind", ":7000", "--workers", "3", "--limit-request-line", "10000", "qlever.wsgi:application"]
CMD [ "gunicorn", "--bind", ":7000", "--workers", "3", "--limit-request-line", "10000", "qlever.wsgi:application" ]

# QLever UI on port 7000 for QLever instance listening on port 7001
#
Expand Down
2 changes: 1 addition & 1 deletion backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def entityNameQueries(self):
def replacePredicatesList(self):
data = {}
for line in self.replacePredicates.split("\n"):
match = re.search("([\S]+)[\s]+([\S]+)", line)
match = re.search(r"([\S]+)[\s]+([\S]+)", line)
if match:
predicate, replacement = match.groups()
data[predicate] = replacement
Expand Down
14 changes: 13 additions & 1 deletion backend/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,20 @@ Custom Elements
}

.tooltip .tooltip-inner { background-color: #82B36F; max-width: 600px; word-wrap: break-word; }
.tooltip .tooltip-arrow { border-top-color: #82B36F !important; }
.tooltip .tooltip-arrow { border-top-color: #82B36F !important; border-bottom-color: #82B36F !important; }

.keyword-search-highlight {
text-decoration: underline;
text-decoration-color: #82B36F;
text-underline-position: under;
text-decoration-thickness: 3px;
}

.keyword-search-hover {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}

.table > tbody > tr > td:first-child { color: #d3d3d3; }
th { color: #82B36F; white-space: nowrap; }
Expand Down
5 changes: 4 additions & 1 deletion backend/static/js/codemirror/modes/sparql/sparql-hint.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,11 @@ function getDynamicSuggestions(context) {
}
}

// Do not launch AC query when current word starts with ? or for subject AC
// queries when the prefix has length < 3.
var sendSparql = !(word.startsWith('?'))
&& !(words.length == 1 && words[0].length < 3);
sparqlQuery = "";
var sendSparql = !(word.startsWith('?'));
var sparqlLines = "";
var mode1Query = ""; // mode 1 is context-insensitive
var mode2Query = ""; // mode 2 is context-sensitive
Expand Down
182 changes: 182 additions & 0 deletions backend/static/js/keywordSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// This variable contains the actual example spans that match the query.
let exampleSpans = {};
// This variable keeps track of the selected example.
let selectedExample = -1;

// This removes highlighting from the input string by replacing all occurrences
// of <span> elements with the class `keyword-search-highlight`
// with their inner content.
function unHighlight(input_str) {
const regex = /\<span class\=\"keyword-search-highlight\"\>(.*?)\<\/span\>/gi;
return input_str.replaceAll(regex, "$1");
}

// This highlights specified words or patterns within an input string
// by wrapping them with a <span> element
// with the class `keyword-search-highlight`.
//
// Algorithm:
// 1. Remove any existing highlighting.
// 2. Iterate over each `regex` in the list of `regexes`
// to find matching sections in the input string.
// 3. Consolidate overlapping sections if any.
// 4. Replace the matching sections with HTML <span> tags for highlighting.
// 5. Return the modified string with highlighted words.
function highlightWords(input_str, regexps) {
let return_str = unHighlight(input_str);
// find matching sections
let matching_sections = [];
for (regexp of regexps) {
const matches = input_str.matchAll(regexp);
for (const match of matches) {
matching_sections.push([match.index, match.index + match[0].length]);
}
}
if (matching_sections.length === 0) {
return return_str;
}
// consolidate overlapping sections
matching_sections.sort((a, b) => a[0] - b[0]);
matching_sections = matching_sections.reduce(
(accu, elem) => {
const [last, ...rest] = accu;
if (elem[0] <= last[1]) {
return [[last[0], Math.max(elem[1], last[1])], ...rest];
}
return [elem].concat(accu);
},
[matching_sections[0]],
);
// replace matching sections with highlighting span
matching_sections.forEach(([from, to]) => {
return_str = `${return_str.substring(0, from)}\
<span class="keyword-search-highlight">${return_str.substring(from, to)}\
</span>${return_str.substring(to)}`;
});
return return_str;
}

// This filters the list of examples given a string
// containing a space sperated list of regexes.
// The filtering is achieved by hiding non-matching examples
// and highlighting matching ones.
function filterExamples(regexes_str) {
const keywords = regexes_str
.trim()
.split(" ")
.filter((keyword) => {
if (keyword === "") {
return false;
}
try {
new RegExp(keyword);
} catch (error) {
if (error instanceof SyntaxError) {
return false;
}
throw error;
}
return true;
})
.map((word) => new RegExp(word, "gi"));
let hits = 0;
exampleSpans.each(function (idx) {
const exampleText = $(this).text().trim();
if (keywords.every((keyword) => exampleText.match(keyword) != null)) {
$(this).addClass("keyword-search-match");
$(this).parent().parent().show();
$(this).html(highlightWords(exampleText, keywords));
hits++;
} else {
$(this).parent().parent().hide();
}
});
exampleSpans = $(".keyword-search-match");
if (hits === 0) {
$("#empty-examples-excuse").show();
} else {
$("#empty-examples-excuse").hide();
}
}

// This creates a debounced version of a function.
// The "debouncing" is implemented by delaying the execution
// for a certain amount of time since the last call.
function debounce(fn, delay = 500) {
let timerId = null;
return (...args) => {
clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
}

const filterExamplesDebounced = debounce(filterExamples, 200);

function cleanup() {
// Calculate the example spans.
exampleSpans = $("ul#exampleList .example-name");
// Reset the selected example to nothing.
selectedExample = -1;
// Remove artifacts from previous usage.
exampleSpans.each(function (idx) {
$(this).removeClass("keyword-search-match");
$(this).parent().parent().show();
$(this).parent().removeClass("keyword-search-hover");
$(this).text(unHighlight($(this).text()));
});
}

$("#exampleKeywordSearchInput").on("keydown", function (event) {
const hover_class = "keyword-search-hover";
// The down key was pressed.
if (exampleSpans.length > 0) {
if (event.which === 40) {
if (exampleSpans.length > 0) {
$(exampleSpans[selectedExample]).parent().removeClass(hover_class);
selectedExample = (selectedExample + 1) % exampleSpans.length;
$(exampleSpans[selectedExample]).parent().addClass(hover_class);
}
}
// The up key was pressed.
else if (event.which === 38) {
$(exampleSpans[selectedExample]).parent().removeClass(hover_class);
selectedExample = selectedExample - 1;
if (selectedExample == -1) {
selectedExample = exampleSpans.length - 1;
}
$(exampleSpans[selectedExample]).parent().addClass(hover_class);
}
// The enter key was pressed.
else if (event.which === 13 && selectedExample >= 0) {
// The timeout of 50ms is used to prevent the keydown event
// to reach the editor. This is a bit of a hack.
setTimeout(() => {
$(exampleSpans[selectedExample]).parent().click();
}, 50);
}
}
// The escape key was pressed.
if (event.which === 27) {
$("#examplesDropdownToggle").click();
}
});

$("#exampleKeywordSearchInput").on(
"input",
debounce(function (event) {
cleanup();
filterExamples(event.target.value);
}, 200),
);

// This initializes the keyword search when the dropdown has loaded.
$("#exampleList")
.parent()
.on("shown.bs.dropdown", function () {
// Clear value of the input field.
$("#exampleKeywordSearchInput").val("");
// Focus the input field.
$("#exampleKeywordSearchInput").focus();
// Cleanup keyword search.
cleanup();
});
27 changes: 22 additions & 5 deletions backend/static/js/qleverUI.js
Original file line number Diff line number Diff line change
Expand Up @@ -831,11 +831,28 @@ function renderRuntimeInformationToDom(entry = undefined) {
$("#visualisation").scrollTop(scrollTop);
$("#result-tree").scrollLeft(scrollLeft);

// For each node, on mouseover show the details.
$("div.node").hover(function () {
$(this).children(".node-details").show();
}, function () {
$(this).children(".node-details").hide();
$("div.node").each(function () {
const details_childs = $(this).children(".node-details");
if (details_childs.length == 1) {
const top_pos = parseFloat($(this).css('top'));
$(this).attr("data-toggle", "tooltip" );
$(this).attr("data-html", "true" );
$(this).attr("data-placement",(top_pos>100?"top":"bottom"));
let detail_html = '';
const details = JSON.parse(details_childs[0].textContent);
for (const key in details) {
detail_html += `<span>${key}: <strong>${details[key]}</strong></span><br>`
}
$(this).attr("title",
`<div style="width: 250px">
<h5> Details </h5>
<hr style="margin-top: 0px; margin-bottom: 0px;">
<div style="margin-top: 10px; margin-bottom: 10px;">
${detail_html}
</div>
</div>`);
$(this).tooltip();
}
});

$("p.node-time").
Expand Down
Loading

0 comments on commit 3c0f234

Please sign in to comment.