From 213abbfe930ad816b75bf8393a42c06707b5e3f7 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Fri, 21 Nov 2025 10:30:14 +1100 Subject: [PATCH] Add community section where public functions can be viewed --- app.py | 2 + db.py | 68 ++++++-- routes/community.py | 50 ++++++ routes/http.py | 6 + services.py | 1 + static/js/mithril/editor.js | 14 ++ templates/community/index.html | 70 ++++++++ templates/community/view.html | 152 ++++++++++++++++++ templates/dashboard.html | 11 ++ .../dashboard/http_functions/editor.html | 1 + 10 files changed, 365 insertions(+), 10 deletions(-) create mode 100644 routes/community.py create mode 100644 templates/community/index.html create mode 100644 templates/community/view.html diff --git a/app.py b/app.py index 4eaf860..0614712 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,7 @@ from routes.http import http from routes.llm import llm from routes.auth import auth from routes.settings import settings +from routes.community import community from constants import DEFAULT_FUNCTION_NAME, DEFAULT_SCRIPT, DEFAULT_ENVIRONMENT from flask_apscheduler import APScheduler import asyncio @@ -45,6 +46,7 @@ app.register_blueprint(http, url_prefix='/http') app.register_blueprint(llm, url_prefix='/llm') app.register_blueprint(auth, url_prefix='/auth') app.register_blueprint(settings, url_prefix='/settings') +app.register_blueprint(community, url_prefix='/community') # Swith to inter app routing, which results in speed up from ~400ms to ~270ms # https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps diff --git a/db.py b/db.py index 2d53687..b97405e 100644 --- a/db.py +++ b/db.py @@ -61,37 +61,55 @@ class DataBase(): 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', + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime, description 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', + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime, description FROM http_functions WHERE user_id=%s ORDER by id DESC', [user_id] ) return http_functions + + def get_public_http_functions(self, search_query=None): + if search_query: + search_pattern = f"%{search_query}%" + http_functions = self.execute( + 'SELECT h.id, h.user_id, h.NAME, h.path, h.script_content, h.invoked_count, h.environment_info, h.is_public, h.log_request, h.log_response, h.version_number, h.runtime, h.description, h.created_at, u.username FROM http_functions h JOIN users u ON h.user_id = u.id WHERE h.is_public=TRUE AND (h.NAME ILIKE %s OR h.description ILIKE %s) ORDER by h.created_at DESC', + [search_pattern, search_pattern] + ) + else: + http_functions = self.execute( + 'SELECT h.id, h.user_id, h.NAME, h.path, h.script_content, h.invoked_count, h.environment_info, h.is_public, h.log_request, h.log_response, h.version_number, h.runtime, h.description, h.created_at, u.username FROM http_functions h JOIN users u ON h.user_id = u.id WHERE h.is_public=TRUE ORDER by h.created_at DESC' + ) + return http_functions def get_http_function(self, user_id, name): http_function = self.execute( - '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) + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime, description 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, 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) + 'SELECT id, user_id, NAME, path, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime, description FROM http_functions WHERE user_id=%s AND id=%s', [user_id, http_function_id], one=True) + return http_function + + def get_public_http_function_by_id(self, http_function_id): + http_function = self.execute( + 'SELECT h.id, h.user_id, h.NAME, h.path, h.script_content, h.invoked_count, h.environment_info, h.is_public, h.log_request, h.log_response, h.version_number, h.created_at, h.runtime, h.description, u.username FROM http_functions h JOIN users u ON h.user_id = u.id WHERE h.id=%s AND h.is_public=TRUE', [http_function_id], one=True) return http_function - def create_new_http_function(self, user_id, name, path, 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, description=""): self.execute( - '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], + 'INSERT INTO http_functions (user_id, NAME, path, script_content, environment_info, is_public, log_request, log_response, runtime, description) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)', + [user_id, name, path, script_content, environment_info, is_public, log_request, log_response, runtime, description], commit=True ) - def edit_http_function(self, user_id, function_id, name, path, 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, description=""): updated_version = self.execute( - '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], + '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, description=%s WHERE user_id=%s AND id=%s RETURNING version_number', + [name, path, script_content, environment_info, is_public, log_request, log_response, runtime, description, user_id, function_id], commit=True, one=True ) return updated_version @@ -115,6 +133,36 @@ FROM http_function_invocations WHERE http_function_id=%s ORDER BY invocation_time DESC""", [http_function_id]) return http_function_invocations + + def fork_http_function(self, user_id, function_id): + # Get the original function + original = self.execute( + 'SELECT NAME, path, script_content, environment_info, runtime, description FROM http_functions WHERE id=%s', + [function_id], + one=True + ) + if not original: + raise Exception("Function not found") + + new_name = original['name'] + # Check if name exists for this user + exists = self.execute('SELECT 1 FROM http_functions WHERE user_id=%s AND NAME=%s', [user_id, new_name], one=True) + if exists: + new_name = f"{new_name}-fork" + + self.create_new_http_function( + user_id, + new_name, + original['path'], + original['script_content'], + original['environment_info'], + False, # is_public + True, # log_request + False, # log_response + original['runtime'], + original['description'] + ) + return new_name def get_user(self, user_id): user = self.execute( diff --git a/routes/community.py b/routes/community.py new file mode 100644 index 0000000..01d4b6c --- /dev/null +++ b/routes/community.py @@ -0,0 +1,50 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify +from flask_login import login_required, current_user +from extensions import db, environment, htmx +from jinja2_fragments import render_block +import json + +community = Blueprint('community', __name__) + +@community.route("/", methods=["GET"]) +@login_required +def index(): + search_query = request.args.get("q", "") + public_functions = db.get_public_http_functions(search_query) + + if htmx: + return render_block( + environment, + "community/index.html", + "page", + public_functions=public_functions, + search_query=search_query + ) + return render_template("community/index.html", public_functions=public_functions, search_query=search_query) + +@community.route("/", methods=["GET"]) +@login_required +def view(function_id): + function = db.get_public_http_function_by_id(function_id) + if not function: + flash("Function not found or not public", "error") + return redirect(url_for("community.index")) + + # Format environment info for display + if function.get('environment_info'): + function['environment_info'] = json.dumps(function['environment_info'], indent=2) + + if htmx: + return render_block(environment, "community/view.html", "page", function=function) + return render_template("community/view.html", function=function) + +@community.route("/fork/", methods=["POST"]) +@login_required +def fork(function_id): + try: + user_id = current_user.id + new_name = db.fork_http_function(user_id, function_id) + flash(f"Function forked as '{new_name}'", "success") + return jsonify({"status": "success", "redirect": url_for("http.overview")}) + except Exception as e: + return jsonify({"status": "error", "message": str(e)}), 400 diff --git a/routes/http.py b/routes/http.py index efab61e..d37462d 100644 --- a/routes/http.py +++ b/routes/http.py @@ -166,6 +166,7 @@ def new(): "is_public": False, "log_request": True, "log_response": False, + "description": "", } if htmx: return render_block( @@ -181,6 +182,7 @@ def new(): log_request = request.json.get("log_request") log_response = request.json.get("log_response") runtime = request.json.get("runtime", "node") + description = request.json.get("description", "") db.create_new_http_function( user_id, @@ -192,6 +194,7 @@ def new(): log_request, log_response, runtime, + description ) return ( @@ -218,6 +221,7 @@ def edit(function_id): log_request = request.json.get("log_request") log_response = request.json.get("log_response") runtime = request.json.get("runtime", "node") + description = request.json.get("description", "") updated_version = db.edit_http_function( user_id, @@ -230,6 +234,7 @@ def edit(function_id): log_request, log_response, runtime, + description ) return {"status": "success", "message": f"{name} updated"} @@ -374,6 +379,7 @@ def editor(function_id): "log_response": http_function["log_response"], "version_number": http_function["version_number"], "runtime": http_function.get("runtime", "node"), + "description": http_function.get("description", ""), "user_id": user_id, "function_id": function_id, # Add new URLs for navigation diff --git a/services.py b/services.py index 06b69d1..c33b97d 100644 --- a/services.py +++ b/services.py @@ -4,6 +4,7 @@ def create_http_function_view_model(http_function): "user_id": http_function['user_id'], "name": http_function['name'], "path": http_function['path'], + "description": http_function['description'], "runtime": http_function['runtime'], "script_content": http_function['script_content'], "invoked_count": http_function['invoked_count'], diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 91654f1..0db4c0d 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -16,6 +16,7 @@ const Editor = { this.name = vnode.attrs.name || "foo"; this.path = vnode.attrs.path || ""; this.versionNumber = vnode.attrs.versionNumber || "1"; + this.description = vnode.attrs.description || ""; this.nameEditing = false; this.pathEditing = false; this.jsValue = vnode.attrs.jsValue || ""; @@ -131,6 +132,7 @@ const Editor = { log_request: this.logRequest, log_response: this.logResponse, runtime: this.runtime, + description: this.description, }; payload = this.isTimer @@ -145,6 +147,7 @@ const Editor = { : null, run_date: this.triggerType === "date" ? this.runDate : null, is_enabled: this.isEnabled, + description: this.description, } : { name: this.name, @@ -155,6 +158,7 @@ const Editor = { log_request: this.logRequest, log_response: this.logResponse, runtime: this.runtime, + description: this.description, }; const response = await m.request({ @@ -603,6 +607,16 @@ const Editor = { this.showFunctionSettings && m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [ m("div", { class: "flex flex-col space-y-4" }, [ + m("div", { class: "flex flex-col space-y-2" }, [ + m("label", { class: "text-sm font-medium text-gray-700 dark:text-gray-300" }, "Description"), + m("textarea", { + class: "w-full p-2 border rounded bg-white dark:bg-gray-700 dark:border-gray-600 text-sm", + rows: 2, + placeholder: "Describe what this function does...", + value: this.description, + oninput: (e) => (this.description = e.target.value) + }) + ]), m("div", { class: "flex flex-wrap gap-6" }, [ this.showPublicToggle && m( diff --git a/templates/community/index.html b/templates/community/index.html new file mode 100644 index 0000000..74ef3a6 --- /dev/null +++ b/templates/community/index.html @@ -0,0 +1,70 @@ +{% extends 'dashboard.html' %} + +{% block page %} +
+
+

Community Library

+
+
+
+ +
+ +
+
+
+ +
+ {% for function in public_functions %} +
+
+
+
{{ function.name }}
+ {{ + function.runtime }} +
+

+ {{ function.description or "No description provided." }} +

+
+ + + + + {{ function.username }} + + {{ function.created_at.strftime('%Y-%m-%d') }} +
+ + View Details + + +
+
+ {% else %} +
+

No public functions found.

+

Be the first to publish one!

+
+ {% endfor %} +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/community/view.html b/templates/community/view.html new file mode 100644 index 0000000..3be17e7 --- /dev/null +++ b/templates/community/view.html @@ -0,0 +1,152 @@ +{% extends 'dashboard.html' %} + +{% block page %} +
+ + + + +
+
+
+
+

{{ function.name }}

+ + {{ function.runtime }} + +
+ +
+
+
+ + + +
+ {{ function.username }} +
+ +
+ + + + Published on {{ function.created_at.strftime('%B %d, %Y') }} +
+
+ + {% if function.description %} +
+

{{ function.description }}

+
+ {% endif %} +
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+
+ +
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index b56330f..2d39636 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -63,6 +63,17 @@ d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> Settings + + + Community diff --git a/templates/dashboard/http_functions/editor.html b/templates/dashboard/http_functions/editor.html index 7b17f42..be97272 100644 --- a/templates/dashboard/http_functions/editor.html +++ b/templates/dashboard/http_functions/editor.html @@ -25,6 +25,7 @@ history_url=url_for('http.history', function_id=function_id)) }} name: '{{ name }}', path: '{{ path }}', functionId: {{ id }}, + description: '{{ description }}', jsValue: {{ script_content | tojson | safe }}, jsonValue: {{ environment_info | tojson | safe }}, isEdit: true,