diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-02-11 17:59:28 +0200 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-02-11 18:04:22 +0200 |
| commit | 001b52fc1b6d2b30b371611f5a8b961e5c6945d3 (patch) | |
| tree | 937b73ccad1aeb927d8974fe703382dafd360768 | |
| parent | e8f3a470ce693bef9303d3dd4c46523cdc691826 (diff) | |
Implement find
| -rw-r--r-- | browser/app.py | 120 | ||||
| -rw-r--r-- | browser/static/htmp.js | 51 | ||||
| -rw-r--r-- | browser/templates/base.html.j2 | 18 | ||||
| -rw-r--r-- | browser/templates/frag_aside.html.j2 | 4 | ||||
| -rw-r--r-- | browser/templates/frag_error.html.j2 | 4 | ||||
| -rw-r--r-- | browser/templates/frag_form_find.html.j2 | 6 | ||||
| -rw-r--r-- | browser/templates/frag_form_range.html.j2 | 2 | ||||
| -rw-r--r-- | browser/templates/frag_results.html.j2 | 18 | ||||
| -rw-r--r-- | browser/templates/page_main.html.j2 | 19 |
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 %} |
