aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2025-02-11 00:25:57 +0200
committerJan Tuomi <jan@jantuomi.fi>2025-02-11 00:25:57 +0200
commitcc07b3fdc4b8860a1ee1b25f09039ba3f13ee509 (patch)
tree7437823d5ff2202912d32c9024640ce993202131
parentff8b5f63ca6bf0bdbfd36edcaed4091d9d25c3de (diff)
Add initial browser UI app
-rw-r--r--browser/app.py69
-rw-r--r--browser/requirements.txt11
-rw-r--r--browser/static/InclusiveSans-Regular.ttfbin0 -> 57124 bytes
-rw-r--r--browser/static/styles.css130
-rw-r--r--browser/templates/aside.html.j216
-rw-r--r--browser/templates/base.html.j220
-rw-r--r--browser/templates/error.html.j26
-rw-r--r--browser/templates/form_find.html.j26
-rw-r--r--browser/templates/form_range.html.j28
-rw-r--r--browser/templates/index.html.j228
10 files changed, 294 insertions, 0 deletions
diff --git a/browser/app.py b/browser/app.py
new file mode 100644
index 0000000..1398e93
--- /dev/null
+++ b/browser/app.py
@@ -0,0 +1,69 @@
+from dotenv import load_dotenv
+load_dotenv()
+
+import os
+import sys
+import traceback
+import tempfile
+from flask import Flask, render_template
+from flask_compress import Compress # type: ignore
+
+from log_db import DB, Bound
+
+app = Flask(__name__)
+Compress(app)
+
+BASE_URL = os.environ["BASE_URL"]
+DB_DIR = os.environ["DB_DIR"] if "DB_DIR" in os.environ else tempfile.TemporaryDirectory().name
+
+print("""\n"""
+ f"""BASE_URL: {BASE_URL}\n"""
+ f"""DB_DIR: {DB_DIR}\n""")
+
+db = DB \
+ .configure() \
+ .data_dir(DB_DIR) \
+ .fields(["id", "name"]) \
+ .primary_key("id") \
+ .secondary_keys(["name"]) \
+ .initialize()
+
+def error_page(message: str, code: int = 400):
+ return render_template("error.html.j2", error = message), code
+
+@app.errorhandler(404)
+def not_found(e: Exception):
+ return error_page("Not found", 404)
+
+@app.errorhandler(405)
+def method_not_allowed(e: Exception):
+ return error_page(str(e), 405)
+
+@app.errorhandler(Exception)
+def error_handler(e: Exception):
+ traceback.print_exception(e, file=sys.stderr)
+ return error_page("Internal server error", 500)
+
+@app.get("/")
+def index():
+ return page_find()
+
+@app.get("/find")
+def page_find():
+ rows = db.range_by("id", Bound.unbounded(), Bound.unbounded(), limit=100)
+
+ return render_template('index.html.j2',
+ selected_form = "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('index.html.j2',
+ selected_form = "form_range.html.j2",
+ field_names = ["id", "name"],
+ rows = rows,
+ )
diff --git a/browser/requirements.txt b/browser/requirements.txt
new file mode 100644
index 0000000..b6b9bff
--- /dev/null
+++ b/browser/requirements.txt
@@ -0,0 +1,11 @@
+blinker==1.9.0
+Brotli==1.1.0
+click==8.1.8
+Flask==3.1.0
+Flask-Compress==1.17
+itsdangerous==2.2.0
+Jinja2==3.1.5
+MarkupSafe==3.0.2
+python-dotenv==1.0.1
+Werkzeug==3.1.3
+zstandard==0.23.0
diff --git a/browser/static/InclusiveSans-Regular.ttf b/browser/static/InclusiveSans-Regular.ttf
new file mode 100644
index 0000000..3a30e79
--- /dev/null
+++ b/browser/static/InclusiveSans-Regular.ttf
Binary files differ
diff --git a/browser/static/styles.css b/browser/static/styles.css
new file mode 100644
index 0000000..73379e7
--- /dev/null
+++ b/browser/static/styles.css
@@ -0,0 +1,130 @@
+:root {
+ --blue: rgb(30, 99, 248);
+ --red: #d42f2f;
+ --yellow: #ff9800;
+ --black: rgb(0, 0, 0);
+ --white: rgb(255, 255, 255);
+ --gray: #666;
+ --light-gray-1: #f2f2f2;
+ --light-gray-2: #f8f6ff;
+ --gold-trans: #ffcd0455;
+
+ --font-family: "Inclusive Sans", sans-serif;
+}
+
+@font-face {
+ font-family: "Inclusive Sans";
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(/static/InclusiveSans-Regular.ttf) format("woff2");
+}
+
+html {
+ font-family: var(--font-family) !important;
+ font-size: 16px;
+}
+
+body {
+ padding: 10px;
+ margin: 0 auto;
+ line-height: 24px;
+}
+
+main {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ align-items: flex-start;
+}
+
+a,
+.contrast {
+ color: var(--blue);
+}
+
+h1 {
+ margin-bottom: 40px;
+}
+
+h1 a {
+ color: var(--black);
+ text-decoration: none;
+}
+
+h1:hover a {
+ border-bottom: 2px dotted var(--blue);
+ text-decoration: none;
+}
+
+textarea,
+input {
+ font-family: var(--font-family);
+ font-size: 16px;
+}
+
+button,
+input[type="submit"] {
+ cursor: pointer;
+ font-size: 16px;
+ border-radius: 4px;
+ text-shadow: none;
+ box-shadow: none;
+ padding: 5px 8px;
+ border: 0;
+ background-color: var(--blue);
+ color: var(--white);
+
+ margin-top: 20px;
+}
+
+button.red,
+input[type="submit"].red {
+ background-color: var(--red);
+}
+
+button.blue,
+input[type="submit"].blue {
+ background-color: var(--blue);
+}
+
+button.yellow,
+input[type="submit"].yellow {
+ background-color: var(--yellow);
+}
+
+input[type="text"],
+input[type*="date"] {
+ font-size: 16px;
+ color: var(--black);
+}
+
+input[type="text"]:disabled,
+input[type*="date"]:disabled {
+ color: var(--gray);
+}
+
+main > aside {
+ width: 250px;
+}
+
+main > .results {
+ flex: 1;
+ overflow-y: scroll;
+ max-height: 100%;
+}
+
+table {
+ width: 100%;
+}
+
+th,
+td {
+ min-width: 100px;
+ text-align: left;
+ padding: 7px 5px;
+}
+
+tr:nth-of-type(odd) td {
+ background-color: var(--light-gray-2);
+}
diff --git a/browser/templates/aside.html.j2 b/browser/templates/aside.html.j2
new file mode 100644
index 0000000..eb8c9cf
--- /dev/null
+++ b/browser/templates/aside.html.j2
@@ -0,0 +1,16 @@
+{% block aside %}
+<aside>
+ <h3>Read operations</h3>
+ <ul>
+ <li><a href="/find">Find</a></li>
+ <li><a href="/range">Range</a></li>
+ </ul>
+ <h3>Write operations</h3>
+ <ul>
+ <li><a href="/upsert">Upsert</a></li>
+ <li><a href="/delete">Delete</a></li>
+ </ul>
+
+ {% include selected_form %}
+</aside>
+{% endblock %}
diff --git a/browser/templates/base.html.j2 b/browser/templates/base.html.j2
new file mode 100644
index 0000000..a8a6cf7
--- /dev/null
+++ b/browser/templates/base.html.j2
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link rel="preload" href="/static/InclusiveSans-Regular.ttf" as="font" type="font/woff2" crossorigin="anonymous">
+ <link rel="stylesheet" href="/static/styles.css"></link>
+ {% block head_meta %}
+ <title>Database browser</title>
+ <meta name="description" content="Database browser for log_db">
+ {% endblock %}
+
+ <!-- -- https://leanrada.com/htmz/ -->
+ <iframe hidden name=htmz onload="setTimeout(()=>document.querySelector(contentWindow.location.hash||null)?.replaceWith(...contentDocument.body.childNodes))"></iframe>
+</head>
+<body>
+ <h1><a href="/">Database browser</a></h1>
+ {% block content %}{% endblock %}
+</body>
+</html>
diff --git a/browser/templates/error.html.j2 b/browser/templates/error.html.j2
new file mode 100644
index 0000000..2d0dc19
--- /dev/null
+++ b/browser/templates/error.html.j2
@@ -0,0 +1,6 @@
+{% extends "base.html.j2" %}
+
+{% block content %}
+<h2>Error</h2>
+{{ error }}
+{% endblock %}
diff --git a/browser/templates/form_find.html.j2 b/browser/templates/form_find.html.j2
new file mode 100644
index 0000000..54608b6
--- /dev/null
+++ b/browser/templates/form_find.html.j2
@@ -0,0 +1,6 @@
+<form action="/find" method="post">
+ <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>
+ <button type="submit">Run query</button>
diff --git a/browser/templates/form_range.html.j2 b/browser/templates/form_range.html.j2
new file mode 100644
index 0000000..76eb4d2
--- /dev/null
+++ b/browser/templates/form_range.html.j2
@@ -0,0 +1,8 @@
+<form action="/range" method="post">
+ <label for="field">Field</label>
+ <input type="text" name="field" id="field" required>
+ <label for="values">From...</label>
+ <input type="text" name="from" id="from" required>
+ <label for="values">...To</label>
+ <input type="text" name="to" id="to" required>
+ <button type="submit">Run query</button>
diff --git a/browser/templates/index.html.j2 b/browser/templates/index.html.j2
new file mode 100644
index 0000000..9713944
--- /dev/null
+++ b/browser/templates/index.html.j2
@@ -0,0 +1,28 @@
+{% extends "base.html.j2" %}
+
+{% block content %}
+<main>
+ {% include "aside.html.j2" %}
+
+ <div class="results">
+ <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>
+ </div>
+</main>
+{% endblock %}