diff --git a/web_query_builder/app.py b/web_query_builder/app.py new file mode 100644 index 0000000..6790e60 --- /dev/null +++ b/web_query_builder/app.py @@ -0,0 +1,55 @@ +from flask import ( + Flask, + render_template, + request, + redirect, + Response, +) +import requests +from flask_cors import CORS + +from werkzeug.middleware.proxy_fix import ProxyFix + +app = Flask(__name__) +CORS(app, resources={r"/api/*": {"origins": "*"}}) + +# This is required because when running in k8s the flask server sits behind a TLS proxy +# So flask speaks http while the client speaks https +# Client <-- https ---> Proxy <---- http ---> Flask server +# For the Oauth flow, flask needs to provide a callback url and it needs to use the right scheme=https +# This line tells flask to look at HTTP headers set by the TLS proxy to figure out what the original +# Traffic looked like. +# See https://flask.palletsprojects.com/en/3.0.x/deploying/proxy_fix/ +app.wsgi_app = ProxyFix( + app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 +) + +config = {} + +@app.route("/") +def index(): + return render_template("index.html", request = request, config = config) + + + +# @app.route('/stac', methods=["GET", "POST"]) # ref. https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6 +# def redirect_to_API_HOST(): #NOTE var :subpath will be unused as all path we need will be read from :request ie from flask import request +# url = f'http://localhost:8124/stac' +# res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616 +# method = request.method, +# url = url, +# headers = {k:v for k,v in request.headers if k.lower() != 'host'}, # exclude 'host' header +# data = request.get_data(), +# cookies = request.cookies, +# allow_redirects = False, +# ) + +# excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1 +# headers = [ +# (k,v) for k,v in res.raw.headers.items() +# if k.lower() not in excluded_headers +# ] + +# response = Response(res.content, res.status_code, headers) +# return response + diff --git a/web_query_builder/requirements.txt b/web_query_builder/requirements.txt new file mode 100644 index 0000000..b75273f --- /dev/null +++ b/web_query_builder/requirements.txt @@ -0,0 +1,7 @@ +flask==3 +pyyaml +flask_dance +python-dotenv +flask-login +flask-cors +cachetools \ No newline at end of file diff --git a/web_query_builder/run.sh b/web_query_builder/run.sh new file mode 100755 index 0000000..d1a12b8 --- /dev/null +++ b/web_query_builder/run.sh @@ -0,0 +1 @@ +flask run --debug --port=5005 \ No newline at end of file diff --git a/web_query_builder/static/app.js b/web_query_builder/static/app.js new file mode 100644 index 0000000..a89853b --- /dev/null +++ b/web_query_builder/static/app.js @@ -0,0 +1,308 @@ +// app.js + +// Take the query string and stick it on the API URL +function getSTACUrlFromQuery() { + const params = new URLSearchParams(window.location.search); + + // get current window url and remove path part + let api_url = new URL(window.location.href); + api_url.pathname = "/stac"; + + for (const [key, value] of params.entries()) { + api_url.searchParams.set(key, value); + } + + console.log(api_url.toString()); + return api_url.toString(); +} + +function get_request_from_url() { + // Extract the query params in order and split any with a , delimiter + // request is an ordered array of [key, [value1, value2, value3, ...]] + const url = new URL(window.location.href); + const params = new URLSearchParams(url.search); + const request = []; + for (const [key, value] of params.entries()) { + request.push([key, value.split(",")]); + } + return request; +} + +function make_url_from_request(request) { + const url = new URL(window.location.href); + url.search = ""; // Clear existing params + const params = new URLSearchParams(); + + for (const [key, values] of request) { + params.set(key, values.join(",")); + } + url.search = params.toString(); + + return url.toString().replace(/%2C/g, ","); +} + +function goToPreviousUrl() { + let request = get_request_from_url(); + request.pop(); + console.log("Request:", request); + const url = make_url_from_request(request); + console.log("URL:", url); + window.location.href = make_url_from_request(request); +} + +// Function to generate a new STAC URL based on current selection +function goToNextUrl() { + const request = get_request_from_url(); + + // Get the currently selected key = value,value2,value3 pairs + const items = Array.from(document.querySelectorAll("div#items > div")); + + let any_new_keys = false; + const new_keys = items.map((item) => { + const key = item.dataset.key; + const key_type = item.dataset.keyType; + let values = []; + + const datePicker = item.querySelector("input[type='date']"); + if (datePicker) { + values.push(datePicker.value.replace(/-/g, "")); + } + + const timePicker = item.querySelector("input[type='time']"); + if (timePicker) { + values.push(timePicker.value.replace(":", "")); + } + + const enum_checkboxes = item.querySelectorAll("input[type='checkbox']:checked"); + if (enum_checkboxes.length > 0) { + values.push(...Array.from(enum_checkboxes).map((checkbox) => checkbox.value)); + } + + const any = item.querySelector("input[type='text']"); + if (any && any.value !== "") { + values.push(any.value); + } + + // Keep track of whether any new keys are selected + if (values.length > 0) { + any_new_keys = true; + } + + return { key, values }; + }); + + // if not new keys are selected, do nothing + if (!any_new_keys) { + return; + } + + // Update the request with the new keys + for (const { key, values } of new_keys) { + // Find the index of the existing key in the request array + const existingIndex = request.findIndex( + ([existingKey, existingValues]) => existingKey === key + ); + + if (existingIndex !== -1) { + // If the key already exists, + // and the values aren't already in there, + // append the values + request[existingIndex][1] = [...request[existingIndex][1], ...values]; + } else { + // If the key doesn't exist, add a new entry + request.push([key, values]); + } + } + + const url = make_url_from_request(request); + window.location.href = url; +} + +async function createCatalogItem(link, itemsContainer) { + const itemDiv = document.createElement("div"); + itemDiv.className = "item loading"; + itemDiv.textContent = "Loading..."; + itemsContainer.appendChild(itemDiv); + + try { + + // Update the item div with real content + itemDiv.classList.remove("loading"); + + const dimension = link["generalized_datacube:dimension"]; + + // add data-key attribute to the itemDiv + itemDiv.dataset.key = link.title; + itemDiv.dataset.keyType = dimension.type; + + itemDiv.innerHTML = ` +
Key Type: ${itemDiv.dataset.keyType || "Unknown"}
+ +Optional: ${dimension.optional ? "Yes" : "No"}
+${dimension.description ? dimension.description.slice(0, 100) : "No description available"}...
+ `; + + + // if (dimension.type === "date" || dimension.type === "time") { + // // Render a date picker for the "date" key + // const picker = ``; + // //convert picker to HTML node + // const pickerNode = document + // .createRange() + // .createContextualFragment(picker); + // itemDiv.appendChild(pickerNode); + // } + // Otherwise create a scrollable list with checkboxes for values if available + if ( + // dimension.type === "enum" && + dimension.values && + dimension.values.length > 0 + ) { + const listContainer = renderCheckboxList(link); + itemDiv.appendChild(listContainer); + } else { + const any = ``; + const anyNode = document.createRange().createContextualFragment(any); + itemDiv.appendChild(anyNode); + } + } catch (error) { + console.error("Error loading item data:", error); + itemDiv.innerHTML = `Error loading item details: ${error}
`; + } +} + +function renderCheckboxList(link) { + const dimension = link["generalized_datacube:dimension"]; + const value_descriptions = dimension.value_descriptions || []; + + const listContainerHTML = ` +{{ config.get('message', '')}}
+Select one or multiple items and then click next to iteratively build up a full request.
+Last database update:
+ + +
+{
+}
+
+
+
+
+
+
+
+