diff --git a/backend/static/js/helper.js b/backend/static/js/helper.js index 68cbbb5..c8ffa49 100644 --- a/backend/static/js/helper.js +++ b/backend/static/js/helper.js @@ -106,8 +106,10 @@ function appendRuntimeInformation(runtime_info, query, time, queryUpdate) { } // Add query time to meta info. - runtime_info["meta"]["total_time_computing"] = - parseInt(time["computeResult"].toString().replace(/ms/, ""), 10); + if ("computeResult" in time) { + runtime_info["meta"]["total_time_computing"] = + parseInt(time["computeResult"].toString().replace(/ms/, ""), 10); + } runtime_info["meta"]["total_time"] = parseInt(time["total"].toString().replace(/ms/, ""), 10); @@ -755,6 +757,12 @@ function expandEditor() { } } +function displayInErrorBlock(message) { + $('#errorReason').html(message); + $('#errorBlock').show(); + $('#answerBlock, #infoBlock, #updatedBlock').hide(); +} + function displayError(response, queryId = undefined) { console.error("Either the GET request failed or the backend returned an error:", response); if (response["exception"] == undefined || response["exception"] == "") { @@ -782,9 +790,7 @@ function displayError(response, queryId = undefined) { queryToDisplay = htmlEscape(queryToDisplay); } disp += "Your query was: " + "
" + queryToDisplay + "
"; - $('#errorReason').html(disp); - $('#errorBlock').show(); - $('#answerBlock, #infoBlock').hide(); + displayInErrorBlock(disp); // If error response contains query and runtime info, append to runtime log. // @@ -811,7 +817,7 @@ function displayWarning(result) { } function displayStatus(str) { - $("#errorBlock,#answerBlock,#warningBlock").hide(); + $("#errorBlock,#answerBlock,#warningBlock,#updatedBlock").hide(); $("#info").html(str); $("#infoBlock").show(); } diff --git a/backend/static/js/qleverUI.js b/backend/static/js/qleverUI.js index 3895136..fd75d32 100755 --- a/backend/static/js/qleverUI.js +++ b/backend/static/js/qleverUI.js @@ -365,6 +365,21 @@ $(document).ready(function () { document.execCommand("copy"); $(this).parent().parent().parent().find(".ok-text").collapse("show"); }); + + const accessToken = $("#access_token"); + + function updateBackendCommandVisibility() { + if (accessToken.val().trim() === "") { + $("#backend_commands").hide(); + } else { + $("#backend_commands").show(); + } + } + + updateBackendCommandVisibility(); + accessToken.on("input", function () { + updateBackendCommandVisibility(); + }); }); @@ -493,221 +508,337 @@ function createWebSocketForQuery(queryId, startTimeStamp, query) { return ws; } +// Determines the type of the operation (Query or Update) and also subtype +// (Select, Insert Data, etc.) as an object `{type: ..., subtype: ...}`. +function determineOperationType(operation) { + // Strip all PREFIX, BASE and WITH statements from the beginning of the query + const strippedOp = operation + .trim() + .replace(/BASE\s+<[^<]*>\s*/gi, "") + .replace(/PREFIX\s+\w+:\s+<[^<]*>\s*/gi, "") + .replace(/WITH\s+((<[^<]*>)|(\w*:\w*))\s*/gi, ""); + const words = strippedOp.split(/\s+/).map(word => word.toUpperCase()) + // Determine the query type based on the first keywords + switch (words[0]) { + case "SELECT": + return {type: "Query", subtype: "Select"}; + case "CONSTRUCT": + return {type: "Query", subtype: "Construct"}; + case "DESCRIBE": + return {type: "Query", subtype: "Describe"}; + case "ASK": + return {type: "Query", subtype: "Ask"}; + case "LOAD": + return {type: "Update", subtype: "Load"}; + case "CLEAR": + return {type: "Update", subtype: "Clear"}; + case "DROP": + return {type: "Update", subtype: "Drop"}; + case "CREATE": + return {type: "Update", subtype: "Create"}; + case "ADD": + return {type: "Update", subtype: "Add"}; + case "MOVE": + return {type: "Update", subtype: "Move"}; + case "COPY": + return {type: "Update", subtype: "Copy"}; + case "INSERT": + if (words[1] === "DATA") { + return {type: "Update", subtype: "Insert Data"}; + } + return {type: "Update", subtype: "Modify"}; + case "DELETE": + if (words[1] === "DATA") { + return {type: "Update", subtype: "Delete Data"}; + } + else if (words[1] === "WHERE") { + return {type: "Update", subtype: "Delete Where"}; + } + return {type: "Update", subtype: "Modify"}; + default: + return {type: "Unknown", subtype: "Unknown"}; + } +} + +function setRunningIndicator(element) { + const icon = $(element).find('.glyphicon'); + icon.addClass('glyphicon-spin glyphicon-refresh'); + icon.removeClass('glyphicon-remove'); + icon.css('color', $(element).css('color')); +} + +function removeRunningIndicator(element) { + const icon = $(element).find('.glyphicon'); + icon.removeClass('glyphicon-spin'); + if (icon.css("color") !== 'rgb(255, 0, 0)') { + icon.css('color', ''); + } +} + +function setErrorIndicator(element) { + const icon = $(element).find('.glyphicon'); + icon.removeClass('glyphicon-refresh'); + icon.addClass('glyphicon-remove'); + icon.css('color', 'red'); +} + +// Executes a backend command (e.g. `clear-cache`). +async function executeBackendCommand(command, element) { + log("Executing command: " + command); + let headers = {}; + const access_token = $.trim($("#access_token").val()); + if (access_token.length > 0) + headers["Authorization"] = `Bearer ${access_token}`; + const params = {"cmd": command}; + setRunningIndicator(element); + try { + await fetchQleverBackend(params, headers); + } catch (error) { + setErrorIndicator(element) + } finally { + removeRunningIndicator(element); + } +} + // Process the given query. async function processQuery(sendLimit=0, element=$("#exebtn")) { log('Preparing query...', 'other'); log('Element: ' + element, 'other'); if (sendLimit >= 0) { displayStatus("Waiting for response..."); } - $(element).find('.glyphicon').addClass('glyphicon-spin glyphicon-refresh'); - $(element).find('.glyphicon').removeClass('glyphicon-remove'); - $(element).find('.glyphicon').css('color', $(element).css('color')); + setRunningIndicator(element); log('Sending request...', 'other'); - // A negative value for `sendLimit` has the special meaning: clear the cache - // (without issuing a query). This is used in `backend/templates/index.html`, - // in the definition of the `oncklick` action for the "Clear cache" button. - // TODO: super ugly, find a better solution. - let nothingToShow = false; - var params = {}; - if (sendLimit >= 0) { - var original_query = editor.getValue(); - var query = await rewriteQuery(original_query, { "name_service": "if_checked" }); - params["query"] = query; - if (sendLimit > 0) { - params["send"] = sendLimit; - } - } else { - params["cmd"] = "clear-cache"; - nothingToShow = true; + let params = {}; + let headers = {}; + let operationType; + console.assert(sendLimit >= 0); + var original_query = editor.getValue(); + var query = await rewriteQuery(original_query, {"name_service": "if_checked"}); + operationType = determineOperationType(query); + console.log(`Determined operation type: ${JSON.stringify(operationType)}`); + switch (operationType.type) { + case "Query": + params["query"] = query; + break; + case "Update": + params["update"] = query; + const access_token = $.trim($("#access_token").val()); + if (access_token.length > 0) headers["Authorization"] = `Bearer ${access_token}`; + break + default: + console.log("Unknown operation type", operationType); + disp = "

Error processing operation

"; + disp += "Could not determine operation type (Query or Update). Please report this operation to us."; + displayInErrorBlock(disp); + setErrorIndicator(element); + removeRunningIndicator(element); + return; + } + if (sendLimit > 0) { + params["send"] = sendLimit; } - const headers = {}; let ws = null; let queryId = undefined; - if (!nothingToShow) { - if (currentlyActiveQueryWebSocket !== null) { - throw new Error("Started a new query before previous one finished!"); - } - queryId = generateQueryId(); - const startTimeStamp = Date.now(); - signalQueryStart(queryId, startTimeStamp, params["query"]); - ws = createWebSocketForQuery(queryId, startTimeStamp, params["query"]); - currentlyActiveQueryWebSocket = ws; - headers["Query-Id"] = queryId; + if (currentlyActiveQueryWebSocket !== null) { + throw new Error("Started a new query before previous one finished!"); } - + queryId = generateQueryId(); + const startTimeStamp = Date.now(); + signalQueryStart(queryId, startTimeStamp, params["query"]); + ws = createWebSocketForQuery(queryId, startTimeStamp, params["query"]); + currentlyActiveQueryWebSocket = ws; + headers["Query-Id"] = queryId; + try { const result = await fetchQleverBackend(params, headers); log('Evaluating and displaying results...', 'other'); - // For non-query commands like "cmd=clear-cache" just remove the "Waiting - // for response box" and that's it. - if (nothingToShow) { - $("#infoBlock").hide(); - return; - } - if (result.status == "ERROR") { displayError(result, queryId); return; } if (result["warnings"].length > 0) { displayWarning(result); } - // Show some statistics (on top of the table). - // - // NOTE: The result size reported by QLever (in the - // application/qlever-results+json format) is the result size without - // without LIMIT. - var nofRows = result.res.length; - const [totalTime, computeTime, resolveTime] = getResultTime(result.time); - let resultSize = result.resultsize; - let limitMatch = result.query.match(/\bLIMIT\s+(\d+)\s*$/); - if (limitMatch) { resultSize = parseInt(limitMatch[1]); } - let resultSizeString = tsep(resultSize.toString()); - $('#resultSize').html(resultSizeString); - $('#totalTime').html(totalTime); - $('#computationTime').html(computeTime); - $('#jsonTime').html(resolveTime); - - const columns = result.selected; - - // If more than predefined number of results, create "Show all" button - // (onclick action defined further down). - let showAllButton = ""; - if (nofRows < parseInt(resultSize)) { - showAllButton = "" - + " " - + "Limited to " + nofRows + " results; show all " + resultSizeString + " results"; - } + switch (operationType.type) { + case "Update": + $('#answerBlock, #infoBlock, #errorBlock').hide(); + const inserted = result["delta-triples"].difference.inserted; + const deleted = result["delta-triples"].difference.deleted; + let updateMessage = `Update successful. (insert triples: ${inserted}, delete triples: ${deleted})`; + $('#updateMetadata').html(updateMessage); + $('#updatedBlock').show(); + $("html, body").animate({ + scrollTop: $("#updatedBlock").scrollTop() + 500 + }, 500); + + // MAX_VALUE ensures this always has priority over the websocket updates + appendRuntimeInformation(result.runtimeInformation, result.update, result.time, { + queryId, + updateTimeStamp: Number.MAX_VALUE + }); + renderRuntimeInformationToDom(); + break + case "Query": + + // Show some statistics (on top of the table). + // + // NOTE: The result size reported by QLever (in the + // application/qlever-results+json format) is the result size without + // without LIMIT. + var nofRows = result.res.length; + const [totalTime, computeTime, resolveTime] = getResultTime(result.time); + let resultSize = result.resultsize; + let limitMatch = result.query.match(/\bLIMIT\s+(\d+)\s*$/); + if (limitMatch) { + resultSize = parseInt(limitMatch[1]); + } + let resultSizeString = tsep(resultSize.toString()); + $('#resultSize').html(resultSizeString); + $('#totalTime').html(totalTime); + $('#computationTime').html(computeTime); + $('#jsonTime').html(resolveTime); + + const columns = result.selected; + + // If more than predefined number of results, create "Show all" button + // (onclick action defined further down). + let showAllButton = ""; + if (nofRows < parseInt(resultSize)) { + showAllButton = "" + + " " + + "Limited to " + nofRows + " results; show all " + resultSizeString + " results"; + } - // If the last column of the first result row contains a WKT literal, - // create "Map View" buttons. - let mapViewButtonVanilla = ''; - let mapViewButtonPetri = ''; - if (result.res.length > 0 && /wktLiteral/.test(result.res[0][columns.length - 1])) { - let mapViewUrlVanilla = 'http://qlever.cs.uni-freiburg.de/mapui/index.html?'; - let params = new URLSearchParams({ query: normalizeQuery(query), backend: BASEURL }); - mapViewButtonVanilla = ` Map view`; - mapViewButtonPetri = ` Map view`; - } + // If the last column of the first result row contains a WKT literal, + // create "Map View" buttons. + let mapViewButtonVanilla = ''; + let mapViewButtonPetri = ''; + if (result.res.length > 0 && /wktLiteral/.test(result.res[0][columns.length - 1])) { + let mapViewUrlVanilla = 'http://qlever.cs.uni-freiburg.de/mapui/index.html?'; + let params = new URLSearchParams({query: normalizeQuery(query), backend: BASEURL}); + mapViewButtonVanilla = ` Map view`; + mapViewButtonPetri = ` Map view`; + } - // Show the buttons (if there are any). - // - // TODO: Exactly which "MapView" buttons are shown depends on the - // instance. How is currently hard-coded. This should be configurable (in - // the Django configuration of the respective backend). - var res = "
"; - if (showAllButton || (mapViewButtonVanilla && mapViewButtonPetri)) { - if (MAP_VIEW_BASE_URL.length > 0) { - res += `
${showAllButton} ${mapViewButtonPetri}
`; - } else { - res += `
${showAllButton}
`; - } - } + // Show the buttons (if there are any). + // + // TODO: Exactly which "MapView" buttons are shown depends on the + // instance. How is currently hard-coded. This should be configurable (in + // the Django configuration of the respective backend). + var res = "
"; + if (showAllButton || (mapViewButtonVanilla && mapViewButtonPetri)) { + if (MAP_VIEW_BASE_URL.length > 0) { + res += `
${showAllButton} ${mapViewButtonPetri}
`; + } else { + res += `
${showAllButton}
`; + } + } - // Optionally show links to other SPARQL endpoints. - // NOTE: we want the *original* query here, as it appears in the editor, - // without the QLever-specific rewrites (see above). - if (SLUG.startsWith("wikidata")) { - const queryEncoded = encodeURIComponent(original_query); - const wdqsUrl = `https://query.wikidata.org/#${queryEncoded}`; - const wdqsButton = ` Query WDQS`; - const virtuosoUrl = "http://wikidata.demo.openlinksw.com/sparql?"; - const virtuosoParams = new URLSearchParams({ - "default-graph-uri": "http://www.wikidata.org/", - "qtxt": original_query, // use "query" instead of "qtxt" to execute query directly - "format": "text/html", - "timeout": 0, - "signal_void": "on" - }); - const virtuosoButton = ` Query Virtuoso`; - res += `
${wdqsButton}
`; - res += `
${virtuosoButton}
`; - } - if (SLUG.startsWith("uniprot")) { - const virtuosoUrl = "http://sparql.uniprot.org/sparql?"; - const virtuosoParams = new URLSearchParams({ - "qtxt": original_query, - "format": "text/html", - "timeout": 0, - "signal_void": "on" - }); - const virtuosoButton = ` Query Virtuoso`; - res += `
${virtuosoButton}
`; - } - if (SLUG.startsWith("dbpedia")) { - const virtuosoUrl = "https://dbpedia.org/sparql?"; - const virtuosoParams = new URLSearchParams({ - "default-graph-uri": "http://dbpedia.org", - "qtxt": original_query, // use "query" instead of "qtxt" to execute query directly - "format": "text/html", - "timeout": 0, - "signal_void": "on" - }); - const virtuosoButton = ` Query Virtuoso`; - res += `
${virtuosoButton}
`; - } + // Optionally show links to other SPARQL endpoints. + // NOTE: we want the *original* query here, as it appears in the editor, + // without the QLever-specific rewrites (see above). + if (SLUG.startsWith("wikidata")) { + const queryEncoded = encodeURIComponent(original_query); + const wdqsUrl = `https://query.wikidata.org/#${queryEncoded}`; + const wdqsButton = ` Query WDQS`; + const virtuosoUrl = "http://wikidata.demo.openlinksw.com/sparql?"; + const virtuosoParams = new URLSearchParams({ + "default-graph-uri": "http://www.wikidata.org/", + "qtxt": original_query, // use "query" instead of "qtxt" to execute query directly + "format": "text/html", + "timeout": 0, + "signal_void": "on" + }); + const virtuosoButton = ` Query Virtuoso`; + res += `
${wdqsButton}
`; + res += `
${virtuosoButton}
`; + } + if (SLUG.startsWith("uniprot")) { + const virtuosoUrl = "http://sparql.uniprot.org/sparql?"; + const virtuosoParams = new URLSearchParams({ + "qtxt": original_query, + "format": "text/html", + "timeout": 0, + "signal_void": "on" + }); + const virtuosoButton = ` Query Virtuoso`; + res += `
${virtuosoButton}
`; + } + if (SLUG.startsWith("dbpedia")) { + const virtuosoUrl = "https://dbpedia.org/sparql?"; + const virtuosoParams = new URLSearchParams({ + "default-graph-uri": "http://dbpedia.org", + "qtxt": original_query, // use "query" instead of "qtxt" to execute query directly + "format": "text/html", + "timeout": 0, + "signal_void": "on" + }); + const virtuosoButton = ` Query Virtuoso`; + res += `
${virtuosoButton}
`; + } - // Leave some space to the actual result table. - res += "


"; + // Leave some space to the actual result table. + res += "


"; - $("#answer").html(res); - $("#show-all").click(() => processQuery().catch(error => log(error.message, "requests"))); + $("#answer").html(res); + $("#show-all").click(() => processQuery().catch(error => log(error.message, "requests"))); - var tableHead = $('#resTable thead'); - var head = ""; - for (var column of columns) { - if (column) { head += "" + column + ""; } - } - head += ""; - tableHead.html(head); - var tableBody = $('#resTable tbody'); - tableBody.html(""); - var i = 1; - for (var resultLine of result.res) { - var row = ""; - row += "" + i + ""; - var j = 0; - for (var resultEntry of resultLine) { - if (resultEntry) { - const [formattedResultEntry, rightAlign] = getFormattedResultEntry(resultEntry, 50, j); - const tooltipText = htmlEscape(resultEntry).replace(/\"/g, """); - row += "" - + "" - + formattedResultEntry + ""; - } else { - row += "-"; + var tableHead = $('#resTable thead'); + var head = ""; + for (var column of columns) { + if (column) { + head += "" + column + ""; + } } - j++; - } - row += ""; - tableBody.append(row); - i++; + head += ""; + tableHead.html(head); + var tableBody = $('#resTable tbody'); + tableBody.html(""); + var i = 1; + for (var resultLine of result.res) { + var row = ""; + row += "" + i + ""; + var j = 0; + for (var resultEntry of resultLine) { + if (resultEntry) { + const [formattedResultEntry, rightAlign] = getFormattedResultEntry(resultEntry, 50, j); + const tooltipText = htmlEscape(resultEntry).replace(/\"/g, """); + row += "" + + "" + + formattedResultEntry + ""; + } else { + row += "-"; + } + j++; + } + row += ""; + tableBody.append(row); + i++; + } + $('[data-toggle="tooltip"]').tooltip(); + $('#infoBlock,#errorBlock,#updatedBlock').hide(); + $('#answerBlock').show(); + $("html, body").animate({ + scrollTop: $("#resTable").scrollTop() + 500 + }, 500); + + // MAX_VALUE ensures this always has priority over the websocket updates + appendRuntimeInformation(result.runtimeInformation, result.query, result.time, { queryId, updateTimeStamp: Number.MAX_VALUE }); + renderRuntimeInformationToDom(); } - $('[data-toggle="tooltip"]').tooltip(); - $('#infoBlock,#errorBlock').hide(); - $('#answerBlock').show(); - $("html, body").animate({ - scrollTop: $("#resTable").scrollTop() + 500 - }, 500); - - // MAX_VALUE ensures this always has priority over the websocket updates - appendRuntimeInformation(result.runtimeInformation, result.query, result.time, { queryId, updateTimeStamp: Number.MAX_VALUE }); - renderRuntimeInformationToDom(); } catch (error) { - $(element).find('.glyphicon').removeClass('glyphicon-refresh'); - $(element).find('.glyphicon').addClass('glyphicon-remove'); - $(element).find('.glyphicon').css('color', 'red'); + setErrorIndicator(element); const errorContent = { "exception" : error.message || "Unknown error", "query": query }; - displayError(errorContent, nothingToShow ? undefined : queryId); + displayError(errorContent, queryId); } finally { currentlyActiveQueryWebSocket = null; - $(element).find('.glyphicon').removeClass('glyphicon-spin'); + removeRunningIndicator(element); // Make sure we have no socket that stays open forever if (ws) { closeWebSocket(ws); @@ -798,7 +929,7 @@ function renderRuntimeInformationToDom(entry = undefined) { const time_index_scans_query_planning = "time_index_scans_query_planning" in meta_info ? formatInteger(meta_info["time_index_scans_query_planning"]) + " ms" : "[not available]"; - const total_time_computing = formatInteger(meta_info["total_time_computing"]); + const total_time_computing = meta_info["total_time_computing"] ? formatInteger(meta_info["total_time_computing"]): "N/A"; $("#meta-info").html( "

Time for query planning: " + time_query_planning + "
Time for index scans during query planning: " + time_index_scans_query_planning + diff --git a/backend/templates/index.html b/backend/templates/index.html index 5dd5c07..327bfea 100644 --- a/backend/templates/index.html +++ b/backend/templates/index.html @@ -91,6 +91,21 @@

Basic settings

Languages:{{ backend.filteredLanguage }} Default maximum{{ backend.maxDefault }} rows Default mode: {% if backend.dynamicSuggestions == 0 %}1. SPARQL syntax & keywords only{% elif backend.dynamicSuggestions == 1 %}2. SPARQL & context insensitive entities{% else %}3. SPARQL & context sensitive entities{% endif %} + + Access Token + + + +
@@ -193,7 +208,7 @@

QLever UI Shortcuts:

-
+ +
+
+ +
+ +
@@ -327,7 +343,11 @@

Query results:

- + + + {% comment %} Comment this in in to re-enable the cheat sheet, disabled 2019 {% include 'partials/help.html' %}