diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 4a0bcc4..94f75ce 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: stac-server +name: qubed description: A Helm chart for the STAC Server with frontend, STAC API and caching service. type: application version: 0.1.0 diff --git a/chart/templates/ingress.yaml b/chart/templates/ingress.yaml index 68b3f07..f20bf83 100644 --- a/chart/templates/ingress.yaml +++ b/chart/templates/ingress.yaml @@ -10,7 +10,7 @@ spec: http: paths: {{- if .Values.stacServer.enabled }} - - path: /api + - path: / pathType: Prefix backend: service: @@ -18,15 +18,6 @@ spec: port: number: {{ .Values.stacServer.servicePort }} {{- end }} - {{- if .Values.webQueryBuilder.enabled }} - - path: / - pathType: Prefix - backend: - service: - name: web-query-builder - port: - number: {{ .Values.webQueryBuilder.servicePort }} - {{- end }} tls: - hosts: - {{ .Values.ingress.hostname }} diff --git a/chart/values.yaml b/chart/values.yaml index 6f9ef8c..4763e9b 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -6,14 +6,6 @@ stacServer: pullPolicy: Always servicePort: 80 -webQueryBuilder: - enabled: true - image: - repository: "eccr.ecmwf.int/qubed/web_query_builder" - tag: "latest" - pullPolicy: Always - servicePort: 80 - ingress: enabled: True tlsSecretName: "lumi-wildcard-tls" diff --git a/dockerfile b/dockerfile index e3b62ec..aebfb4f 100644 --- a/dockerfile +++ b/dockerfile @@ -34,12 +34,3 @@ COPY ./stac_server /code/stac_server WORKDIR /code/stac_server CMD ["fastapi", "dev", "main.py", "--proxy-headers", "--port", "80", "--host", "0.0.0.0"] - -FROM base AS web_query_builder - -COPY web_query_builder/requirements.txt /code/requirements.txt -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt - -COPY web_query_builder /code/web_query_builder -WORKDIR /code/web_query_builder -CMD ["flask", "run", "--host", "0.0.0.0", "--port", "80"] diff --git a/scripts/build_images.sh b/scripts/build_images.sh index 173ccf1..f48e04f 100755 --- a/scripts/build_images.sh +++ b/scripts/build_images.sh @@ -7,9 +7,3 @@ sudo docker build \ --target=stac_server \ . sudo docker push eccr.ecmwf.int/qubed/stac_server:latest - -sudo docker build \ - --tag=eccr.ecmwf.int/qubed/web_query_builder:latest \ - --target=web_query_builder \ - . -sudo docker push eccr.ecmwf.int/qubed/web_query_builder:latest diff --git a/stac_server/static/app.js b/stac_server/static/app.js index 87758f8..8e11a73 100644 --- a/stac_server/static/app.js +++ b/stac_server/static/app.js @@ -176,7 +176,6 @@ function renderCheckboxList(link) { const listContainerHTML = `
-
${variable.enum .map((value, index) => { diff --git a/stac_server/static/styles.css b/stac_server/static/styles.css index 4e7a028..7458e00 100644 --- a/stac_server/static/styles.css +++ b/stac_server/static/styles.css @@ -2,6 +2,9 @@ html, body { min-height: 100vh; height: 100%; + + --accent-color: #003399; + --background-grey: #f4f4f4; } body { @@ -23,7 +26,7 @@ body { width: 30%; padding: 10px; overflow-y: scroll; - background-color: #f4f4f4; + background-color: var(--background-grey); border-right: 1px solid #ddd; } @@ -65,6 +68,7 @@ canvas { margin-bottom: 10px; border-radius: 5px; transition: background-color 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .item-title { @@ -93,10 +97,8 @@ canvas { } .item.selected { - background-color: #d4e9ff; - /* Lighter blue for selection */ - border-color: #003399; - /* Keep the original ECMWF blue for the border */ + background-color: var(--background-grey); + border-color: var(--accent-color); } summary h2 { @@ -119,7 +121,7 @@ button { /* Padding around button text */ margin: 0 5px; /* Margin between buttons */ - background-color: #003399; + background-color: var(--accent-color); /* ECMWF blue */ color: white; /* White text color */ @@ -140,7 +142,6 @@ button:hover { .item-list-container { margin-top: 20px; - margin-bottom: 20px; } .scrollable-list { @@ -150,7 +151,6 @@ button:hover { border: 1px solid #ccc; border-radius: 4px; background-color: #fff; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .checkbox-container { @@ -170,14 +170,14 @@ button:hover { } .checkbox-container:hover .checkbox-label { - color: #003399; + color: var(--accent-color); } .list-label { font-weight: bold; margin-bottom: 0.5em; display: block; - color: #003399; + color: var(--accent-color); } span.key, @@ -214,4 +214,4 @@ span.value:hover { details h2 { font-size: medium; -} \ No newline at end of file +} diff --git a/stac_server/templates/index.html b/stac_server/templates/index.html index 633013f..e9c780a 100644 --- a/stac_server/templates/index.html +++ b/stac_server/templates/index.html @@ -41,6 +41,7 @@

Currently Selected Tree

+

This shows the data qube that matches with the current query. The leaves are the next set if available selections you can make.


 
             
diff --git a/web_query_builder/.env b/web_query_builder/.env deleted file mode 100644 index d6ae525..0000000 --- a/web_query_builder/.env +++ /dev/null @@ -1 +0,0 @@ -API_HOST=localhost:8124 diff --git a/web_query_builder/app.py b/web_query_builder/app.py deleted file mode 100644 index 2c7af2b..0000000 --- a/web_query_builder/app.py +++ /dev/null @@ -1,33 +0,0 @@ -import os - -from flask import ( - Flask, - render_template, - request, -) -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, - api_url=os.environ.get("API_URL", "/api/v1/stac"), - ) diff --git a/web_query_builder/requirements.txt b/web_query_builder/requirements.txt deleted file mode 100644 index 96f3517..0000000 --- a/web_query_builder/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -flask==3 -pyyaml -flask_dance -python-dotenv -flask-login -flask-cors -cachetools -uvicorn diff --git a/web_query_builder/run.sh b/web_query_builder/run.sh deleted file mode 100755 index d3ec221..0000000 --- a/web_query_builder/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -export API_URL="http://127.0.0.1:8124/api/v1/stac/climate-dt" -flask run --debug --port=5006 diff --git a/web_query_builder/static/app.js b/web_query_builder/static/app.js deleted file mode 100644 index a9eccb8..0000000 --- a/web_query_builder/static/app.js +++ /dev/null @@ -1,314 +0,0 @@ -// 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 - if (window.API_URL.startsWith("http")) { - // Absolute URL: Use it directly - api_url = new URL(window.API_URL); - } else { - // Relative URL: Combine with the current window's location - api_url = new URL(window.location.href); - api_url.pathname = window.API_URL; - } - - 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 variables = link["variables"]; - const key = Object.keys(variables)[0]; - const variable = variables[key]; - - // add data-key attribute to the itemDiv - itemDiv.dataset.key = link.title; - itemDiv.dataset.keyType = variable.type; - - itemDiv.innerHTML = ` -

${link.title || "No title available"}

-

Key Type: ${itemDiv.dataset.keyType || "Unknown"}

-

${ - variable.description ? variable.description.slice(0, 100) : "" - }

- `; - - if (variable.enum && variable.enum.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 variables = link["variables"]; - const key = Object.keys(variables)[0]; - const variable = variables[key]; - const value_descriptions = variable.value_descriptions || []; - - const listContainerHTML = ` -
- -
- ${variable.enum - .map((value, index) => { - const labelText = value_descriptions[index] - ? `${value} - ${value_descriptions[index]}` - : value; - return ` -
- -
- `; - }) - .join("")} -
-
- `; - - return document.createRange().createContextualFragment(listContainerHTML) - .firstElementChild; -} - -// Render catalog items in the sidebar -function renderCatalogItems(links) { - const itemsContainer = document.getElementById("items"); - itemsContainer.innerHTML = ""; // Clear previous items - - console.log("Number of Links:", links); - const children = links.filter( - (link) => link.rel === "child" || link.rel === "items" - ); - console.log("Number of Children:", children.length); - - children.forEach((link) => { - createCatalogItem(link, itemsContainer); - }); -} - -function renderRequestBreakdown(request, descriptions) { - const container = document.getElementById("request-breakdown"); - const format_value = (key, value) => { - return `"${value}"`; - }; - - const format_values = (key, values) => { - if (values.length === 1) { - return format_value(key, values[0]); - } - return `[${values.map((v) => format_value(key, v)).join(", ")}]`; - }; - - let html = - `{\n` + - request - .map( - ([key, values]) => - ` "${key}": ${format_values(key, values)},` - ) - .join("\n") + - `\n}`; - container.innerHTML = html; -} - -function renderRawSTACResponse(catalog) { - const itemDetails = document.getElementById("raw-stac"); - // create new object without debug key - let just_stac = Object.assign({}, catalog); - delete just_stac.debug; - itemDetails.textContent = JSON.stringify(just_stac, null, 2); - - const debug_container = document.getElementById("debug"); - debug_container.textContent = JSON.stringify(catalog.debug, null, 2); - - const qube_container = document.getElementById("qube"); - qube_container.innerHTML = catalog.debug.qube; -} - -// Fetch STAC catalog and display items -async function fetchCatalog(request, stacUrl) { - try { - const response = await fetch(stacUrl); - const catalog = await response.json(); - - // Render the request breakdown in the sidebar - renderRequestBreakdown(request, catalog.debug.descriptions); - - // Show the raw STAC in the sidebar - renderRawSTACResponse(catalog); - - // Render the items from the catalog - if (catalog.links) { - console.log("Fetched STAC catalog:", stacUrl, catalog.links); - renderCatalogItems(catalog.links); - } - - // Highlight the request and raw STAC - hljs.highlightElement(document.getElementById("raw-stac")); - hljs.highlightElement(document.getElementById("debug")); - } catch (error) { - console.error("Error fetching STAC catalog:", error); - } -} - -// Initialize the viewer by fetching the STAC catalog -function initializeViewer() { - const stacUrl = getSTACUrlFromQuery(); - const request = get_request_from_url(); - - if (stacUrl) { - console.log("Fetching STAC catalog from query string URL:", stacUrl); - fetchCatalog(request, stacUrl); - } else { - console.error("No STAC URL provided in the query string."); - } - - // Add event listener for the "Generate STAC URL" button - const generateUrlBtn = document.getElementById("next-btn"); - generateUrlBtn.addEventListener("click", goToNextUrl); - - const previousUrlBtn = document.getElementById("previous-btn"); - previousUrlBtn.addEventListener("click", goToPreviousUrl); - - // Add event listener for the "Raw STAC" button - const stacAnchor = document.getElementById("stac-anchor"); - stacAnchor.href = getSTACUrlFromQuery(); -} - -// Call initializeViewer on page load -initializeViewer(); diff --git a/web_query_builder/static/qube_styles.css b/web_query_builder/static/qube_styles.css deleted file mode 100644 index 3e8868e..0000000 --- a/web_query_builder/static/qube_styles.css +++ /dev/null @@ -1,50 +0,0 @@ -pre#qube { - font-family: monospace; - white-space: pre; - font-family: SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace; - font-size: 12px; - line-height: 1.4; - - details { - margin-left: 0; - } - - .qubed-node a { - margin-left: 10px; - text-decoration: none; - } - - summary { - list-style: none; - cursor: pointer; - text-overflow: ellipsis; - overflow: hidden; - text-wrap: nowrap; - display: block; - } - - summary:hover,span.leaf:hover { - background-color: #f0f0f0; - } - - details > summary::after { - content: ' ▲'; - } - - details:not([open]) > summary::after { - content: " ▼"; - } - - .leaf { - text-overflow: ellipsis; - overflow: hidden; - text-wrap: nowrap; - display: block; - } - - summary::-webkit-details-marker { - display: none; - content: ""; - } - -} diff --git a/web_query_builder/static/styles.css b/web_query_builder/static/styles.css deleted file mode 100644 index 0b48cbb..0000000 --- a/web_query_builder/static/styles.css +++ /dev/null @@ -1,215 +0,0 @@ -html, -body { - min-height: 100vh; - height: 100%; -} - -body { - font-family: Arial, sans-serif; - margin: 0; - padding-left: 0.5em; - padding-right: 0.5em; - -} - -#viewer { - display: flex; - flex-direction: row; - height: fit-content; - min-height: 100vh; -} - -#catalog-list { - width: 30%; - padding: 10px; - overflow-y: scroll; - background-color: #f4f4f4; - border-right: 1px solid #ddd; -} - -#catalog-list h2 { - margin-top: 0; -} - -#details { - width: 70%; - padding: 10px; -} - -.sidebar-header { - display: flex; - justify-content: center; - margin-bottom: 10px; - flex-wrap: wrap; - gap: 0.5em; -} - -.sidebar-header button { - width: 10em; -} - -canvas { - width: 100%; - height: 300px; - border: 1px solid #ccc; - margin-top: 20px; -} - -/* Updated CSS for the item elements in the catalog list */ -.item { - background-color: white; - border: 1px solid #ddd; - padding: 10px; - margin-bottom: 10px; - border-radius: 5px; - transition: background-color 0.2s ease; -} - -.item-title { - font-size: 18px; - margin: 0; - color: #333; -} - -.item-type { - font-size: 14px; - margin: 5px 0; - color: #666; -} - -.item-id, -.item-key-type { - font-size: 12px; - color: #999; -} - -.item-description { - font-size: 13px; - margin: 5px 0; - color: #444; - font-style: italic; -} - -.item.selected { - background-color: #d4e9ff; - /* Lighter blue for selection */ - border-color: #003399; - /* Keep the original ECMWF blue for the border */ -} - -summary h2 { - display: inline; -} - -.json-pre { - white-space: pre-wrap; - /* background-color: #f9f9f9; */ - border: 1px solid #ccc; - border-radius: 5px; - padding: 10px; -} - - -/* Button styles */ -button { - height: 3em; - padding: 10px 20px; - /* Padding around button text */ - margin: 0 5px; - /* Margin between buttons */ - background-color: #003399; - /* ECMWF blue */ - color: white; - /* White text color */ - border: none; - /* Remove default button border */ - cursor: pointer; - /* Pointer cursor on hover */ - border-radius: 5px; - /* Rounded corners */ - transition: background-color 0.3s ease; - /* Smooth background color transition */ -} - -button:hover { - background-color: #001f66; - /* Darker shade of ECMWF blue on hover */ -} - -.item-list-container { - margin-top: 20px; - margin-bottom: 20px; -} - -.scrollable-list { - max-height: 200px; - overflow-y: auto; - padding: 10px; - border: 1px solid #ccc; - border-radius: 4px; - background-color: #fff; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); -} - -.checkbox-container { - display: flex; - align-items: center; - margin-bottom: 10px; -} - -.item-checkbox { - margin-right: 10px; - cursor: pointer; -} - -.checkbox-label { - font-size: 16px; - color: #333; -} - -.checkbox-container:hover .checkbox-label { - color: #003399; -} - -.list-label { - font-weight: bold; - margin-bottom: 0.5em; - display: block; - color: #003399; -} - -span.key, -span.value { - color: #ba2121; - ; -} - -span.key { - font-weight: bold; -} - -span.key:hover, -span.value:hover { - color: #ff2a2a; - cursor: pointer; -} - -/* Change layout for narrow viewport */ -@media (max-width: 800px) { - #viewer { - flex-direction: column; - } - - #catalog-list { - width: 100%; - border-right: none; - } - - #details { - width: 100%; - } -} - -details h2 { - font-size: medium; -} diff --git a/web_query_builder/templates/index.html b/web_query_builder/templates/index.html deleted file mode 100644 index 3743bf1..0000000 --- a/web_query_builder/templates/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - ECMWF DestinE STAC Viewer - - - - - - - - - - -
-
-

STAC Items

-

{{ config.get('message', '')}}

-

Select one or multiple items and then click next to iteratively build up a full request.

-

Last database update:

- - -
- -
-
-
-

Current Selection

- This is a MARS Selection object in JSON format. Hover over a key or value for more info. - -

-{
-}
-            
- - -

Currently Selected Tree

-

-
-            
-            
-

Raw STAC Response

-

See the STAC Extension Proposal for more details on the format.

-
-
- - -
-

Debug Info

-
-
-
-
- - - - -