diff --git a/db.py b/db.py index f949df2..a3c032c 100644 --- a/db.py +++ b/db.py @@ -57,32 +57,41 @@ class DataBase(): finally: cur.close() - def get_http_functions_for_user(self, user_id): - http_functions = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime FROM http_functions WHERE user_id=%s ORDER by id DESC', [user_id]) + def get_http_functions_for_user(self, user_id, search_query=None): + if search_query: + search_pattern = f"%{search_query}%" + http_functions = self.execute( + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime FROM http_functions WHERE user_id=%s AND (NAME ILIKE %s OR path ILIKE %s) ORDER by id DESC', + [user_id, search_pattern, search_pattern] + ) + else: + http_functions = self.execute( + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime FROM http_functions WHERE user_id=%s ORDER by id DESC', + [user_id] + ) return http_functions def get_http_function(self, user_id, name): http_function = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND NAME=%s', [user_id, name], one=True) + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND NAME=%s', [user_id, name], one=True) return http_function def get_http_function_by_id(self, user_id, http_function_id): http_function = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND id=%s', [user_id, http_function_id], one=True) + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND id=%s', [user_id, http_function_id], one=True) return http_function - def create_new_http_function(self, user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime): + def create_new_http_function(self, user_id, name, path, script_content, environment_info, is_public, log_request, log_response, runtime): self.execute( - 'INSERT INTO http_functions (user_id, NAME, script_content, environment_info, is_public, log_request, log_response, runtime) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)', - [user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime], + 'INSERT INTO http_functions (user_id, NAME, path, script_content, environment_info, is_public, log_request, log_response, runtime) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)', + [user_id, name, path, script_content, environment_info, is_public, log_request, log_response, runtime], commit=True ) - def edit_http_function(self, user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response, runtime): + def edit_http_function(self, user_id, function_id, name, path, script_content, environment_info, is_public, log_request, log_response, runtime): updated_version = self.execute( - 'UPDATE http_functions SET NAME=%s, script_content=%s, environment_info=%s, is_public=%s, log_request=%s, log_response=%s, runtime=%s WHERE user_id=%s AND id=%s RETURNING version_number', - [name, script_content, environment_info, is_public, log_request, log_response, runtime, user_id, function_id], + 'UPDATE http_functions SET NAME=%s, path=%s, script_content=%s, environment_info=%s, is_public=%s, log_request=%s, log_response=%s, runtime=%s WHERE user_id=%s AND id=%s RETURNING version_number', + [name, path, script_content, environment_info, is_public, log_request, log_response, runtime, user_id, function_id], commit=True, one=True ) return updated_version diff --git a/routes/http.py b/routes/http.py index 96c1f7c..efab61e 100644 --- a/routes/http.py +++ b/routes/http.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone, timedelta import json from services import create_http_function_view_model, create_http_functions_view_model -''' +""" CREATE TABLE http_function_versions ( id SERIAL PRIMARY KEY, http_function_id INT NOT NULL, @@ -66,7 +66,7 @@ AFTER INSERT OR UPDATE ON http_functions FOR EACH ROW EXECUTE PROCEDURE fn_http_functions_versioning(); -''' +""" DEFAULT_SCRIPT = """async (req) => { console.log(`Method:${req.method}`) @@ -104,18 +104,52 @@ DEFAULT_PYTHON_SCRIPT = """def main(request, environment): return {"body": "Hello from Python!"} """ -http = Blueprint('http', __name__) +http = Blueprint("http", __name__) + + +def group_functions_by_path(functions): + grouped = {} + for function in functions: + # Use the explicit path column + full_path = function.get("path", "") or "" + path_parts = [p for p in full_path.split("/") if p] + + current_level = grouped + for part in path_parts: + if part not in current_level: + current_level[part] = {} + current_level = current_level[part] + + function["_is_function"] = True + current_level[function["name"]] = function + return grouped @http.route("/overview", methods=["GET"]) @login_required def overview(): user_id = current_user.id - http_functions = db.get_http_functions_for_user(user_id) - http_functions = create_http_functions_view_model(http_functions) - if htmx: - return render_block(environment, "dashboard/http_functions/overview.html", "page", http_functions=http_functions) - return render_template("dashboard/http_functions/overview.html", http_functions=http_functions) + search_query = request.args.get("q", "") + + http_functions = db.get_http_functions_for_user(user_id, search_query) + http_functions_view_model = create_http_functions_view_model(http_functions) + grouped_functions = group_functions_by_path(http_functions_view_model) + if request.headers.get('HX-Target') == 'function-list': + return render_block(environment, "dashboard/http_functions/overview.html", "function_list", http_functions=grouped_functions) + + if htmx: + return render_block( + environment, + "dashboard/http_functions/overview.html", + "page", + http_functions=grouped_functions, + search_query=search_query + ) + return render_template( + "dashboard/http_functions/overview.html", + http_functions=grouped_functions, + search_query=search_query + ) @http.route("/new", methods=["GET", "POST"]) @login_required @@ -123,36 +157,52 @@ def new(): user_id = current_user.id if request.method == "GET": context = { - 'user_id': user_id, - 'name': 'foo', - 'script': DEFAULT_SCRIPT, - 'default_python_script': DEFAULT_PYTHON_SCRIPT, - 'environment_info': DEFAULT_ENVIRONMENT, - 'is_public': False, - 'log_request': True, - 'log_response': False + "user_id": user_id, + "name": "foo", + "path": "", + "script": DEFAULT_SCRIPT, + "default_python_script": DEFAULT_PYTHON_SCRIPT, + "environment_info": DEFAULT_ENVIRONMENT, + "is_public": False, + "log_request": True, + "log_response": False, } if htmx: - return render_block(environment, 'dashboard/http_functions/new.html', 'page', **context) + return render_block( + environment, "dashboard/http_functions/new.html", "page", **context + ) return render_template("dashboard/http_functions/new.html", **context) try: - name = request.json.get('name') - script_content = request.json.get('script_content') - environment_info = request.json.get('environment_info') - is_public = request.json.get('is_public') - log_request = request.json.get('log_request') - log_response = request.json.get('log_response') - runtime = request.json.get('runtime', 'node') - - db.create_new_http_function(user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime) + name = request.json.get("name") + path = request.json.get("path", "") + script_content = request.json.get("script_content") + environment_info = request.json.get("environment_info") + is_public = request.json.get("is_public") + log_request = request.json.get("log_request") + log_response = request.json.get("log_response") + runtime = request.json.get("runtime", "node") - return jsonify({ - "status": "success", - "message": "Http function created successfully" - }), 200 + db.create_new_http_function( + user_id, + name, + path, + script_content, + environment_info, + is_public, + log_request, + log_response, + runtime, + ) + + return ( + jsonify( + {"status": "success", "message": "Http function created successfully"} + ), + 200, + ) except Exception as e: print(e) - return { "status": "error", "message": str(e) } + return {"status": "error", "message": str(e)} @http.route("/edit/", methods=["POST"]) @@ -160,20 +210,33 @@ def new(): def edit(function_id): try: user_id = current_user.id - name = request.json.get('name') - script_content = request.json.get('script_content') - environment_info = request.json.get('environment_info') - is_public = request.json.get('is_public') - log_request = request.json.get('log_request') - log_response = request.json.get('log_response') - runtime = request.json.get('runtime', 'node') - - updated_version = db.edit_http_function(user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response, runtime) + name = request.json.get("name") + path = request.json.get("path", "") + script_content = request.json.get("script_content") + environment_info = request.json.get("environment_info") + is_public = request.json.get("is_public") + log_request = request.json.get("log_request") + log_response = request.json.get("log_response") + runtime = request.json.get("runtime", "node") - return { "status": "success", "message": f'{name} updated' } + updated_version = db.edit_http_function( + user_id, + function_id, + name, + path, + script_content, + environment_info, + is_public, + log_request, + log_response, + runtime, + ) + + return {"status": "success", "message": f"{name} updated"} except Exception as e: print(e) - return { "status": "error", "message": str(e) } + return {"status": "error", "message": str(e)} + @http.route("/delete/", methods=["DELETE"]) @login_required @@ -182,11 +245,15 @@ def delete(function_id): user_id = current_user.id db.execute( - 'DELETE FROM http_functions WHERE user_id=%s AND id=%s', [user_id, function_id], commit=True) - - return { "status": "success", "message": f'Function deleted' } + "DELETE FROM http_functions WHERE user_id=%s AND id=%s", + [user_id, function_id], + commit=True, + ) + + return {"status": "success", "message": f"Function deleted"} except Exception as e: - return jsonify({"status": 'error', "message": str(e)}), 500 + return jsonify({"status": "error", "message": str(e)}), 500 + @http.route("/logs/", methods=["GET"]) @login_required @@ -194,12 +261,27 @@ def logs(function_id): user_id = current_user.id http_function = db.get_http_function_by_id(user_id, function_id) if not http_function: - return jsonify({'error': 'Function not found'}), 404 - name = http_function['name'] + return jsonify({"error": "Function not found"}), 404 + name = http_function["name"] http_function_invocations = db.get_http_function_invocations(function_id) if htmx: - return render_block(environment, 'dashboard/http_functions/logs.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function_invocations=http_function_invocations) - return render_template("dashboard/http_functions/logs.html", user_id=user_id, name=name, function_id=function_id, http_function_invocations=http_function_invocations) + return render_block( + environment, + "dashboard/http_functions/logs.html", + "page", + user_id=user_id, + function_id=function_id, + name=name, + http_function_invocations=http_function_invocations, + ) + return render_template( + "dashboard/http_functions/logs.html", + user_id=user_id, + name=name, + function_id=function_id, + http_function_invocations=http_function_invocations, + ) + @http.route("/client/", methods=["GET"]) @login_required @@ -207,49 +289,70 @@ def client(function_id): user_id = current_user.id http_function = db.get_http_function_by_id(user_id, function_id) if not http_function: - return jsonify({'error': 'Function not found'}), 404 - + return jsonify({"error": "Function not found"}), 404 + http_function = create_http_function_view_model(http_function) if htmx: - return render_block(environment, 'dashboard/http_functions/client.html', 'page', function_id=function_id, **http_function) - return render_template("dashboard/http_functions/client.html", function_id=function_id, **http_function) + return render_block( + environment, + "dashboard/http_functions/client.html", + "page", + function_id=function_id, + **http_function, + ) + return render_template( + "dashboard/http_functions/client.html", function_id=function_id, **http_function + ) -@http.route('/history/') + +@http.route("/history/") @login_required def history(function_id): # Fetch the http function to verify ownership - http_function = db.execute(""" + http_function = db.execute( + """ SELECT id, name, script_content AS code, version_number FROM http_functions WHERE id = %s AND user_id = %s - """, [function_id, current_user.id], one=True) + """, + [function_id, current_user.id], + one=True, + ) if not http_function: - flash('Http function not found', 'error') - return redirect(url_for('http.overview')) + flash("Http function not found", "error") + return redirect(url_for("http.overview")) # Fetch all versions - versions = db.execute(""" + versions = db.execute( + """ SELECT version_number, script_content AS script, updated_at AS versioned_at FROM http_functions_versions WHERE http_function_id = %s ORDER BY version_number DESC - """, [function_id]) + """, + [function_id], + ) # Convert datetime objects to ISO format strings for version in versions: - version['versioned_at'] = version['versioned_at'].isoformat() if version['versioned_at'] else None + version["versioned_at"] = ( + version["versioned_at"].isoformat() if version["versioned_at"] else None + ) args = { - 'user_id': current_user.id, - 'function_id': function_id, - 'http_function': http_function, - 'versions': versions + "user_id": current_user.id, + "function_id": function_id, + "http_function": http_function, + "versions": versions, } if htmx: - return render_block(environment, 'dashboard/http_functions/history.html', 'page', **args) - return render_template('dashboard/http_functions/history.html', **args) + return render_block( + environment, "dashboard/http_functions/history.html", "page", **args + ) + return render_template("dashboard/http_functions/history.html", **args) + @http.route("/editor/", methods=["GET"]) @login_required @@ -257,27 +360,30 @@ def editor(function_id): user_id = current_user.id http_function = db.get_http_function_by_id(user_id, function_id) if not http_function: - return jsonify({'error': 'Function not found'}), 404 - + return jsonify({"error": "Function not found"}), 404 + # Create a view model with all necessary data for the editor editor_data = { - 'id': http_function['id'], - 'name': http_function['name'], - 'script_content': http_function['script_content'], - 'environment_info': json.dumps(http_function['environment_info'], indent=2), - 'is_public': http_function['is_public'], - 'log_request': http_function['log_request'], - 'log_response': http_function['log_response'], - 'version_number': http_function['version_number'], - 'runtime': http_function.get('runtime', 'node'), - 'user_id': user_id, - 'function_id': function_id, + "id": http_function["id"], + "name": http_function["name"], + "path": http_function.get("path", ""), + "script_content": http_function["script_content"], + "environment_info": json.dumps(http_function["environment_info"], indent=2), + "is_public": http_function["is_public"], + "log_request": http_function["log_request"], + "log_response": http_function["log_response"], + "version_number": http_function["version_number"], + "runtime": http_function.get("runtime", "node"), + "user_id": user_id, + "function_id": function_id, # Add new URLs for navigation - 'cancel_url': url_for('http.overview'), - 'edit_url': url_for('http.editor', function_id=function_id), + "cancel_url": url_for("http.overview"), + "edit_url": url_for("http.editor", function_id=function_id), } if htmx: - return render_block(environment, "dashboard/http_functions/editor.html", "page", **editor_data) - - return render_template("dashboard/http_functions/editor.html", **editor_data) \ No newline at end of file + return render_block( + environment, "dashboard/http_functions/editor.html", "page", **editor_data + ) + + return render_template("dashboard/http_functions/editor.html", **editor_data) diff --git a/services.py b/services.py index 1b0aad4..06b69d1 100644 --- a/services.py +++ b/services.py @@ -3,6 +3,8 @@ def create_http_function_view_model(http_function): "id": http_function['id'], "user_id": http_function['user_id'], "name": http_function['name'], + "path": http_function['path'], + "runtime": http_function['runtime'], "script_content": http_function['script_content'], "invoked_count": http_function['invoked_count'], "environment_info": http_function['environment_info'], diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 32dfb45..2a08207 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -23,8 +23,10 @@ const Editor = { // Name + version this.name = vnode.attrs.name || "foo"; + this.path = vnode.attrs.path || ""; this.versionNumber = vnode.attrs.versionNumber || "1"; this.nameEditing = false; + this.pathEditing = false; // Editor defaults this.jsValue = vnode.attrs.jsValue || ""; @@ -151,6 +153,7 @@ const Editor = { try { let payload = { name: this.name, + path: this.path, script_content: this.jsValue, environment_info: this.jsonValue, is_public: this.isPublic, @@ -175,6 +178,7 @@ const Editor = { } : { name: this.name, + path: this.path, script_content: this.jsValue, environment_info: this.jsonValue, is_public: this.isPublic, @@ -296,6 +300,27 @@ const Editor = { }, [ m("span", { class: "inline-flex items-center" }, [ + // Path + !this.pathEditing + ? m( + "span", + { + class: "font-mono text-gray-500 mr-1", + onclick: () => (this.pathEditing = true), + }, + this.path ? `${this.path}/` : "/" + ) + : m("input", { + class: + "bg-gray-50 border border-gray-300 text-sm rounded-lg p-1.5 dark:bg-gray-700 dark:border-gray-600 w-24 mr-1", + value: this.path, + placeholder: "path", + oninput: (e) => (this.path = e.target.value), + onblur: () => (this.pathEditing = false), + autofocus: true, + }), + + // Name !this.nameEditing ? m( "span", @@ -348,21 +373,37 @@ const Editor = { : null, // If adding a new function - this.isAdd - ? m("div", { class: "w-full" }, [ - m( - "label", - { class: "block mb-2 text-sm font-medium" }, - "Function name" - ), - m("input", { - type: "text", - class: "bg-gray-50 border w-full p-2.5 rounded-lg", - placeholder: "foo", - required: true, - value: this.name, - oninput: (e) => (this.name = e.target.value), - }), + this.isAdd + ? m("div", { class: "flex space-x-2 w-full" }, [ + m("div", { class: "w-1/3" }, [ + m( + "label", + { class: "block mb-2 text-sm font-medium" }, + "Path" + ), + m("input", { + type: "text", + class: "bg-gray-50 border w-full p-2.5 rounded-lg", + placeholder: "api/v1", + value: this.path, + oninput: (e) => (this.path = e.target.value), + }), + ]), + m("div", { class: "w-2/3" }, [ + m( + "label", + { class: "block mb-2 text-sm font-medium" }, + "Function name" + ), + m("input", { + type: "text", + class: "bg-gray-50 border w-full p-2.5 rounded-lg", + placeholder: "foo", + required: true, + value: this.name, + oninput: (e) => (this.name = e.target.value), + }), + ]), ]) : null, ]) diff --git a/templates/dashboard/http_functions/_function_list.html b/templates/dashboard/http_functions/_function_list.html new file mode 100644 index 0000000..9d58ab3 --- /dev/null +++ b/templates/dashboard/http_functions/_function_list.html @@ -0,0 +1,68 @@ +{% macro render_functions(functions, path_prefix='') %} +
+ {% for name, children in functions.items() %} + {% if children['_is_function'] is not defined %} +
+ + + + + {{ name }} + +
+ {{ render_functions(children, path_prefix + name + '/') }} +
+
+ {% else %} + {% set function = children %} +
+
+ {{ function.name.split('/')[-1] }} + + v{{ function.version_number }} + + + {{ function.runtime }} + + + {{ function.invoked_count }} + +
+
+ + + + + + + + +
+
+ {% endif %} + {% endfor %} +
+{% endmacro %} \ No newline at end of file diff --git a/templates/dashboard/http_functions/editor.html b/templates/dashboard/http_functions/editor.html index 7795af2..7b17f42 100644 --- a/templates/dashboard/http_functions/editor.html +++ b/templates/dashboard/http_functions/editor.html @@ -23,6 +23,7 @@ history_url=url_for('http.history', function_id=function_id)) }} m.mount(document.getElementById("app"), { view: () => m(Editor, { name: '{{ name }}', + path: '{{ path }}', functionId: {{ id }}, jsValue: {{ script_content | tojson | safe }}, jsonValue: {{ environment_info | tojson | safe }}, diff --git a/templates/dashboard/http_functions/new.html b/templates/dashboard/http_functions/new.html index 9a5eead..20580b1 100644 --- a/templates/dashboard/http_functions/new.html +++ b/templates/dashboard/http_functions/new.html @@ -23,6 +23,7 @@ title='New HTTP Function') m.mount(document.getElementById("app"), { view: () => m(Editor, { name: '{{ name }}', + path: '{{ path }}', jsValue: {{ script | tojson | safe }}, pythonValue: {{ default_python_script | tojson | safe }}, jsonValue: {{ environment_info | tojson | safe }}, diff --git a/templates/dashboard/http_functions/overview.html b/templates/dashboard/http_functions/overview.html index c36b5e1..0135df9 100644 --- a/templates/dashboard/http_functions/overview.html +++ b/templates/dashboard/http_functions/overview.html @@ -2,7 +2,7 @@ {% block page %} -
+

HTTP Functions

-
-
- - - - - - - - - - {% for function in http_functions %} - - - - - - {% endfor %} +
+ +
- {% if http_functions|length == 0 %} - - - - {% endif %} - -
NameURL -
-
- {{ function.name }} - - #{{ function.invoked_count }} - - - v{{ function.version_number }} - - {% if function.is_public %} - - - - - - {% endif %} -
-
- -
-

No functions found

-
+
+ {% block function_list %} + {% from 'dashboard/http_functions/_function_list.html' import render_functions %} + {{ render_functions(http_functions) }} + + {% if http_functions|length == 0 %} +
+
+ +

No functions found

+

Get started by creating a new function.

+
+ {% endif %} + {% endblock %}