aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2025-02-11 17:59:28 +0200
committerJan Tuomi <jan@jantuomi.fi>2025-02-11 18:04:22 +0200
commit001b52fc1b6d2b30b371611f5a8b961e5c6945d3 (patch)
tree937b73ccad1aeb927d8974fe703382dafd360768
parente8f3a470ce693bef9303d3dd4c46523cdc691826 (diff)
Implement find
-rw-r--r--browser/app.py120
-rw-r--r--browser/static/htmp.js51
-rw-r--r--browser/templates/base.html.j218
-rw-r--r--browser/templates/frag_aside.html.j24
-rw-r--r--browser/templates/frag_error.html.j24
-rw-r--r--browser/templates/frag_form_find.html.j26
-rw-r--r--browser/templates/frag_form_range.html.j22
-rw-r--r--browser/templates/frag_results.html.j218
-rw-r--r--browser/templates/page_main.html.j219
9 files changed, 177 insertions, 65 deletions
diff --git a/browser/app.py b/browser/app.py
index ebe590d..be00fa4 100644
--- a/browser/app.py
+++ b/browser/app.py
@@ -8,7 +8,8 @@ import tempfile
from flask import Flask, render_template, request
from flask_compress import Compress # type: ignore
-from log_db import DB, Bound
+import log_db
+from log_db import DB, Bound, Value
app = Flask(__name__)
Compress(app)
@@ -20,28 +21,35 @@ print("""\n"""
f"""BASE_URL: {BASE_URL}\n"""
f"""DB_DIR: {DB_DIR}\n""")
+db_fields = ["id", "name"]
+db_types = ["int", "string"]
+
db = DB \
.configure() \
.data_dir(DB_DIR) \
- .fields(["id", "name"]) \
+ .fields(db_fields) \
.primary_key("id") \
.secondary_keys(["name"]) \
.initialize()
-@app.errorhandler(Exception)
-def error_handler(e: Exception):
- if e.code >= 500: # type: ignore
- traceback.print_exception(e, file=sys.stderr)
- error_text = "Internal Server Error"
- else:
- error_text = str(e)
+def error(e: str, code: int):
+ error_text = f"HTTP {code}: {e}"
+ #return render_template("page_error.html.j2", error = error_text)
- htmz_target = request.form.get("htmz")
- if htmz_target:
- return render_template("frag_error.html.j2", error = error_text, container = htmz_target)
+ htmp_target = request.form.get("htmp") or request.args.get("htmp")
+ if htmp_target:
+ return render_template("frag_error.html.j2", error = error_text, container = htmp_target)
else:
return render_template("page_error.html.j2", error = error_text)
+@app.errorhandler(Exception)
+def error_handler(e: Exception):
+ if hasattr(e, "code") and 400 >= getattr(e, "code") < 500:
+ return error(str(e), getattr(e, "code"))
+ else:
+ traceback.print_exception(e, file=sys.stderr)
+ return error("Internal Server Error", 500)
+
@app.get("/")
def index():
return page_find()
@@ -49,19 +57,87 @@ def index():
@app.get("/find")
def page_find():
rows = db.range_by("id", Bound.unbounded(), Bound.unbounded(), limit=100)
+ rows = [[value_to_str(v) for v in row] for row in rows]
+
+ htmp_target = request.form.get("htmp") or request.args.get("htmp")
+ if htmp_target:
+ return render_template("frag_form_find.html.j2")
+ else:
+ return render_template('page_main.html.j2',
+ selected_form = "frag_form_find.html.j2",
+ field_names = ["id", "name"],
+ rows = rows,
+ )
- return render_template('page_main.html.j2',
- selected_form = "frag_form_find.html.j2",
- field_names = ["id", "name"],
- rows = rows,
- )
+@app.post("/find")
+def query_find():
+ field = request.form.get("field")
+ if not field: raise ValueError("Field is required")
+
+ values = request.form.get("values")
+ if not values: raise ValueError("Values are required")
+
+ field_index = db_fields.index(field)
+ if field_index == -1: raise ValueError(f"Field '{field}' not found")
+
+ try:
+ values = [cast_to_value(field, v) for v in values.split("\n")]
+ values = [v for v in values if v is not None]
+ except ValueError as e:
+ return error(str(e), 400)
+
+ tagged_rows = db.batch_find_by(field, values, limit=100)
+ rows = [[value_to_str(v) for v in row] for (_, row) in tagged_rows]
+
+ htmp_target = request.form.get("htmp") or request.args.get("htmp")
+ if htmp_target:
+ return render_template("frag_results.html.j2",
+ field_names = ["id", "name"],
+ rows = rows,
+ )
+ else:
+ return render_template('page_main.html.j2',
+ selected_form = "frag_form_find.html.j2",
+ field_names = ["id", "name"],
+ rows = rows,
+ )
@app.get("/range")
def page_range():
rows = db.range_by("id", Bound.unbounded(), Bound.unbounded(), limit=100)
- return render_template('page_main.html.j2',
- selected_form = "frag_form_range.html.j2",
- field_names = ["id", "name"],
- rows = rows,
- )
+ htmp_target = request.form.get("htmp") or request.args.get("htmp")
+ if htmp_target:
+ return render_template("frag_form_range.html.j2")
+ else:
+ return render_template('page_main.html.j2',
+ selected_form = "frag_form_range.html.j2",
+ field_names = ["id", "name"],
+ rows = rows,
+ )
+
+def cast_to_value(field: str, str_value: str) -> Value | None:
+ str_value = str_value.strip()
+ if str_value == "": return None
+ if str_value[0] == "\"":
+ if str_value[-1] != "\"": raise ValueError(f"Invalid string: {str_value}")
+ str_value = str_value[1:-1]
+
+ field_index = db_fields.index(field)
+ if field_index == -1: raise ValueError(f"Field '{field}' not found")
+
+ type = db_types[field_index]
+
+ match type:
+ case "int": return Value.int(int(str_value))
+ case "string": return Value.string(str_value)
+ case _: raise ValueError(f"Unsupported type: {type}")
+
+def value_to_str(value: Value) -> str:
+ match value.kind():
+ case log_db.VALUE_INT: return f"{str(value.as_int())} (int)"
+ case log_db.VALUE_STRING: return f"\"{value.as_string()}\" (string)"
+ case log_db.VALUE_DECIMAL: return f"{value.as_decimal()} (decimal)"
+ case log_db.VALUE_BYTES: return f"{value.as_bytes()} (bytes)"
+ case log_db.VALUE_NULL: return "null"
+ case _: raise ValueError(f"Unsupported value kind: {value.kind()}")
diff --git a/browser/static/htmp.js b/browser/static/htmp.js
new file mode 100644
index 0000000..36deae2
--- /dev/null
+++ b/browser/static/htmp.js
@@ -0,0 +1,51 @@
+const iframe = document.createElement("iframe");
+iframe.name = "htmp";
+iframe.hidden = true;
+document.body.appendChild(iframe);
+
+document
+ .querySelector("iframe[name='htmp']")
+ .addEventListener("load", function () {
+ setTimeout(() => {
+ const cd = this.contentDocument;
+ const cw = this.contentWindow;
+ // If the server responds with an entire HTML document, replace the current document with it.
+ // To check whether the response is an entire page we check the presence of this very snippet.
+ if (cd.querySelector("iframe[name='htmp']")) {
+ document.documentElement.replaceWith(cd.documentElement);
+ // If the server responds with a fragment, replace the target element with it.
+ } else {
+ document
+ .querySelector(cw.location.hash || null)
+ ?.replaceWith(...cd.body.childNodes);
+ const url = new URL(cw.location.href);
+ url.searchParams.delete("htmp");
+ url.hash = "";
+ history.replaceState({}, "", url.toString());
+ }
+ });
+ });
+
+document.querySelectorAll("form[htmp]").forEach(function (el) {
+ const re = el.attributes.replace.value;
+ const input = document.createElement("input");
+ input.type = "hidden";
+ input.name = "htmp";
+ input.value = re;
+ el.appendChild(input);
+
+ const action = new URL(el.action);
+ action.hash = re;
+ el.action = action.toString();
+ el.target = "htmp";
+});
+
+document.querySelectorAll("a[htmp]").forEach(function (el) {
+ const re = el.attributes.replace.value;
+
+ const href = new URL(el.href);
+ href.hash = re;
+ href.searchParams.set("htmp", re);
+ el.target = "htmp";
+ el.href = href.toString();
+});
diff --git a/browser/templates/base.html.j2 b/browser/templates/base.html.j2
index 8b41a85..47b3f6c 100644
--- a/browser/templates/base.html.j2
+++ b/browser/templates/base.html.j2
@@ -14,22 +14,6 @@
<h1><a href="/">Database browser</a></h1>
{% block content %}{% endblock %}
- <!-- https://leanrada.com/htmz/ -->
- <iframe hidden name=htmz onload="setTimeout(()=>document.querySelector(contentWindow.location.hash||null)?.replaceWith(...contentDocument.body.childNodes))"></iframe>
- <!-- Enable progressive enhancement for htmz -->
- <script>
- document.querySelectorAll("form[htmz]").forEach(function (element) {
- const input = document.createElement("input");
- input.type = "hidden";
- input.name = "htmz";
- input.value = element.attributes.replace.value;
- element.appendChild(input);
-
- const action = new URL(element.action);
- action.hash = element.attributes.replace.value;
- element.action = action.toString();
- element.target = "htmz";
- });
- </script>
+ <script src="/static/htmp.js"></script>
</body>
</html>
diff --git a/browser/templates/frag_aside.html.j2 b/browser/templates/frag_aside.html.j2
index 8366436..7ec10ad 100644
--- a/browser/templates/frag_aside.html.j2
+++ b/browser/templates/frag_aside.html.j2
@@ -1,8 +1,8 @@
<aside id="aside">
<h3>Read operations</h3>
<ul>
- <li><a href="/find">Find</a></li>
- <li><a href="/range">Range</a></li>
+ <li><a href="/find" htmp replace=form_query>Find</a></li>
+ <li><a href="/range" htmp replace=form_query>Range</a></li>
</ul>
<h3>Write operations</h3>
<ul>
diff --git a/browser/templates/frag_error.html.j2 b/browser/templates/frag_error.html.j2
index f014f0a..092642f 100644
--- a/browser/templates/frag_error.html.j2
+++ b/browser/templates/frag_error.html.j2
@@ -1,10 +1,10 @@
{% if container %}
-<{{ container }}>
+<div id="{{ container }}">
{% endif %}
<h2>Error</h2>
{{ error }}
{% if container %}
-</{{ container }}>
+</div>
{% endif %}
diff --git a/browser/templates/frag_form_find.html.j2 b/browser/templates/frag_form_find.html.j2
index a8fe493..cadd44b 100644
--- a/browser/templates/frag_form_find.html.j2
+++ b/browser/templates/frag_form_find.html.j2
@@ -1,7 +1,7 @@
-<form action="/find" method="post" htmz replace=main>
+<form id="form_query" action="/find" method="post" htmp replace=results>
<label for="field">Field</label>
<input type="text" name="field" id="field" required>
- <label for="values">Values</label>
- <input type="text" name="values" id="values" required>
+ <label for="values">Values (one per line)</label>
+ <textarea name="values" id="values" required></textarea>
<button type="submit">Run query</button>
</form>
diff --git a/browser/templates/frag_form_range.html.j2 b/browser/templates/frag_form_range.html.j2
index b343b6d..96fe8a8 100644
--- a/browser/templates/frag_form_range.html.j2
+++ b/browser/templates/frag_form_range.html.j2
@@ -1,4 +1,4 @@
-<form action="/range" method="post" htmz replace=main>
+<form id="form_query" action="/range" method="post" htmp replace=results>
<label for="field">Field</label>
<input type="text" name="field" id="field" required>
<label for="values">From...</label>
diff --git a/browser/templates/frag_results.html.j2 b/browser/templates/frag_results.html.j2
new file mode 100644
index 0000000..bfb54e9
--- /dev/null
+++ b/browser/templates/frag_results.html.j2
@@ -0,0 +1,18 @@
+<table id="results">
+<thead>
+ <tr>
+ {% for field_name in field_names %}
+ <th>{{ field_name }}</th>
+ {% endfor %}
+ </tr>
+</thead>
+<tbody>
+ {% for row in rows %}
+ <tr>
+ {% for field in row %}
+ <td>{{ field }}</td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+</tbody>
+</table>
diff --git a/browser/templates/page_main.html.j2 b/browser/templates/page_main.html.j2
index 8ce4a13..907275f 100644
--- a/browser/templates/page_main.html.j2
+++ b/browser/templates/page_main.html.j2
@@ -5,24 +5,7 @@
{% include "frag_aside.html.j2" %}
<main id="main">
- <table>
- <thead>
- <tr>
- {% for field_name in field_names %}
- <th>{{ field_name }}</th>
- {% endfor %}
- </tr>
- </thead>
- <tbody>
- {% for row in rows %}
- <tr>
- {% for field in row %}
- <td>{{ field }}</td>
- {% endfor %}
- </tr>
- {% endfor %}
- </tbody>
- </table>
+ {% include "frag_results.html.j2" %}
</main>
</div>
{% endblock %}