Add community section where public functions can be viewed

This commit is contained in:
Peter Stockings
2025-11-21 10:30:14 +11:00
parent 8eb9b7dceb
commit 213abbfe93
10 changed files with 365 additions and 10 deletions

2
app.py
View File

@@ -17,6 +17,7 @@ from routes.http import http
from routes.llm import llm from routes.llm import llm
from routes.auth import auth from routes.auth import auth
from routes.settings import settings from routes.settings import settings
from routes.community import community
from constants import DEFAULT_FUNCTION_NAME, DEFAULT_SCRIPT, DEFAULT_ENVIRONMENT from constants import DEFAULT_FUNCTION_NAME, DEFAULT_SCRIPT, DEFAULT_ENVIRONMENT
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
import asyncio import asyncio
@@ -45,6 +46,7 @@ app.register_blueprint(http, url_prefix='/http')
app.register_blueprint(llm, url_prefix='/llm') app.register_blueprint(llm, url_prefix='/llm')
app.register_blueprint(auth, url_prefix='/auth') app.register_blueprint(auth, url_prefix='/auth')
app.register_blueprint(settings, url_prefix='/settings') 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 # 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 # https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps

68
db.py
View File

@@ -61,37 +61,55 @@ class DataBase():
if search_query: if search_query:
search_pattern = f"%{search_query}%" search_pattern = f"%{search_query}%"
http_functions = self.execute( 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] [user_id, search_pattern, search_pattern]
) )
else: else:
http_functions = self.execute( 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] [user_id]
) )
return http_functions 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): def get_http_function(self, user_id, name):
http_function = self.execute( 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 return http_function
def get_http_function_by_id(self, user_id, http_function_id): def get_http_function_by_id(self, user_id, http_function_id):
http_function = self.execute( 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 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( 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)', '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], [user_id, name, path, script_content, environment_info, is_public, log_request, log_response, runtime, description],
commit=True 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( 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', '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, user_id, function_id], [name, path, script_content, environment_info, is_public, log_request, log_response, runtime, description, user_id, function_id],
commit=True, one=True commit=True, one=True
) )
return updated_version return updated_version
@@ -115,6 +133,36 @@ FROM http_function_invocations
WHERE http_function_id=%s WHERE http_function_id=%s
ORDER BY invocation_time DESC""", [http_function_id]) ORDER BY invocation_time DESC""", [http_function_id])
return http_function_invocations 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): def get_user(self, user_id):
user = self.execute( user = self.execute(

50
routes/community.py Normal file
View File

@@ -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("/<int:function_id>", 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/<int:function_id>", 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

View File

@@ -166,6 +166,7 @@ def new():
"is_public": False, "is_public": False,
"log_request": True, "log_request": True,
"log_response": False, "log_response": False,
"description": "",
} }
if htmx: if htmx:
return render_block( return render_block(
@@ -181,6 +182,7 @@ def new():
log_request = request.json.get("log_request") log_request = request.json.get("log_request")
log_response = request.json.get("log_response") log_response = request.json.get("log_response")
runtime = request.json.get("runtime", "node") runtime = request.json.get("runtime", "node")
description = request.json.get("description", "")
db.create_new_http_function( db.create_new_http_function(
user_id, user_id,
@@ -192,6 +194,7 @@ def new():
log_request, log_request,
log_response, log_response,
runtime, runtime,
description
) )
return ( return (
@@ -218,6 +221,7 @@ def edit(function_id):
log_request = request.json.get("log_request") log_request = request.json.get("log_request")
log_response = request.json.get("log_response") log_response = request.json.get("log_response")
runtime = request.json.get("runtime", "node") runtime = request.json.get("runtime", "node")
description = request.json.get("description", "")
updated_version = db.edit_http_function( updated_version = db.edit_http_function(
user_id, user_id,
@@ -230,6 +234,7 @@ def edit(function_id):
log_request, log_request,
log_response, log_response,
runtime, runtime,
description
) )
return {"status": "success", "message": f"{name} updated"} return {"status": "success", "message": f"{name} updated"}
@@ -374,6 +379,7 @@ def editor(function_id):
"log_response": http_function["log_response"], "log_response": http_function["log_response"],
"version_number": http_function["version_number"], "version_number": http_function["version_number"],
"runtime": http_function.get("runtime", "node"), "runtime": http_function.get("runtime", "node"),
"description": http_function.get("description", ""),
"user_id": user_id, "user_id": user_id,
"function_id": function_id, "function_id": function_id,
# Add new URLs for navigation # Add new URLs for navigation

View File

@@ -4,6 +4,7 @@ def create_http_function_view_model(http_function):
"user_id": http_function['user_id'], "user_id": http_function['user_id'],
"name": http_function['name'], "name": http_function['name'],
"path": http_function['path'], "path": http_function['path'],
"description": http_function['description'],
"runtime": http_function['runtime'], "runtime": http_function['runtime'],
"script_content": http_function['script_content'], "script_content": http_function['script_content'],
"invoked_count": http_function['invoked_count'], "invoked_count": http_function['invoked_count'],

View File

@@ -16,6 +16,7 @@ const Editor = {
this.name = vnode.attrs.name || "foo"; this.name = vnode.attrs.name || "foo";
this.path = vnode.attrs.path || ""; this.path = vnode.attrs.path || "";
this.versionNumber = vnode.attrs.versionNumber || "1"; this.versionNumber = vnode.attrs.versionNumber || "1";
this.description = vnode.attrs.description || "";
this.nameEditing = false; this.nameEditing = false;
this.pathEditing = false; this.pathEditing = false;
this.jsValue = vnode.attrs.jsValue || ""; this.jsValue = vnode.attrs.jsValue || "";
@@ -131,6 +132,7 @@ const Editor = {
log_request: this.logRequest, log_request: this.logRequest,
log_response: this.logResponse, log_response: this.logResponse,
runtime: this.runtime, runtime: this.runtime,
description: this.description,
}; };
payload = this.isTimer payload = this.isTimer
@@ -145,6 +147,7 @@ const Editor = {
: null, : null,
run_date: this.triggerType === "date" ? this.runDate : null, run_date: this.triggerType === "date" ? this.runDate : null,
is_enabled: this.isEnabled, is_enabled: this.isEnabled,
description: this.description,
} }
: { : {
name: this.name, name: this.name,
@@ -155,6 +158,7 @@ const Editor = {
log_request: this.logRequest, log_request: this.logRequest,
log_response: this.logResponse, log_response: this.logResponse,
runtime: this.runtime, runtime: this.runtime,
description: this.description,
}; };
const response = await m.request({ const response = await m.request({
@@ -603,6 +607,16 @@ const Editor = {
this.showFunctionSettings && this.showFunctionSettings &&
m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [ 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-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" }, [ m("div", { class: "flex flex-wrap gap-6" }, [
this.showPublicToggle && this.showPublicToggle &&
m( m(

View File

@@ -0,0 +1,70 @@
{% extends 'dashboard.html' %}
{% block page %}
<div class="flex flex-col space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Community Library</h1>
<div class="w-full max-w-sm">
<form class="relative" hx-get="{{ url_for('community.index') }}" hx-target="#container" hx-push-url="true">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg aria-hidden="true" class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none"
stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
<input type="search" name="q"
class="block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Search functions..." value="{{ search_query }}">
</form>
</div>
</div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{% for function in public_functions %}
<div
class="bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700 hover:shadow-lg transition-shadow duration-200">
<div class="p-5">
<div class="flex justify-between items-start mb-2">
<h5 class="text-xl font-bold tracking-tight text-gray-900 dark:text-white truncate"
title="{{ function.name }}">{{ function.name }}</h5>
<span
class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">{{
function.runtime }}</span>
</div>
<p class="mb-3 font-normal text-gray-700 dark:text-gray-400 text-sm line-clamp-2 h-10">
{{ function.description or "No description provided." }}
</p>
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-4">
<span class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 mr-1">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
</svg>
{{ function.username }}
</span>
<span>{{ function.created_at.strftime('%Y-%m-%d') }}</span>
</div>
<a href="#" hx-get="{{ url_for('community.view', function_id=function.id) }}" hx-target="#container"
hx-push-url="true"
class="inline-flex items-center px-3 py-2 text-sm font-medium text-center text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 w-full justify-center">
View Details
<svg aria-hidden="true" class="w-4 h-4 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</a>
</div>
</div>
{% else %}
<div class="col-span-full text-center py-10 text-gray-500 dark:text-gray-400">
<p class="text-lg">No public functions found.</p>
<p class="text-sm">Be the first to publish one!</p>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,152 @@
{% extends 'dashboard.html' %}
{% block page %}
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Back Link -->
<div class="mb-6">
<a href="#" hx-get="{{ url_for('community.index') }}" hx-target="#container" hx-push-url="true"
class="inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4 mr-1">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
Back to Community Library
</a>
</div>
<!-- Header Section -->
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-6">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-3 mb-2">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white tracking-tight">{{ function.name }}</h1>
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300 border border-blue-200 dark:border-blue-800 uppercase tracking-wide">
{{ function.runtime }}
</span>
</div>
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-1.5">
<div class="p-1 rounded-full bg-gray-100 dark:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
class="w-4 h-4 text-gray-600 dark:text-gray-300">
<path fill-rule="evenodd"
d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
clip-rule="evenodd" />
</svg>
</div>
<span class="font-medium text-gray-700 dark:text-gray-300">{{ function.username }}</span>
</div>
<span class="hidden sm:inline text-gray-300 dark:text-gray-600"></span>
<div class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0h18M5.25 12h13.5h-13.5Zm0 3.75h13.5h-13.5Z" />
</svg>
<span>Published on {{ function.created_at.strftime('%B %d, %Y') }}</span>
</div>
</div>
{% if function.description %}
<div class="mt-4 prose prose-sm dark:prose-invert max-w-none text-gray-600 dark:text-gray-300">
<p class="whitespace-pre-wrap">{{ function.description }}</p>
</div>
{% endif %}
</div>
<div class="flex-shrink-0">
<button hx-post="{{ url_for('community.fork', function_id=function.id) }}" hx-target="#container"
class="inline-flex items-center justify-center px-5 py-2.5 text-sm font-medium text-white transition-all bg-green-600 border border-transparent rounded-lg shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-offset-gray-900">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-5 h-5 mr-2 -ml-1">
<path stroke-linecap="round" stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
Fork to My Library
</button>
</div>
</div>
</div>
<!-- Tabs & Content -->
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="border-b border-gray-200 dark:border-gray-700">
<nav class="flex -mb-px" aria-label="Tabs">
<button onclick="switchTab('code')" id="tab-code"
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none transition-colors border-blue-500 text-blue-600 dark:text-blue-400">
Function Code
</button>
<button onclick="switchTab('env')" id="tab-env"
class="w-1/2 py-4 px-1 text-center border-b-2 font-medium text-sm focus:outline-none transition-colors border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300">
Environment Config
</button>
</nav>
</div>
<div class="p-0">
<div id="content-code" class="block">
<div id="code-editor" class="h-[500px] w-full"></div>
</div>
<div id="content-env" class="hidden">
<div id="env-editor" class="h-[500px] w-full"></div>
</div>
</div>
</div>
</div>
<script>
// Tab Switching Logic
window.switchTab = function (tabName) {
// Update Tab Styles
const tabs = ['code', 'env'];
tabs.forEach(t => {
const btn = document.getElementById(`tab-${t}`);
const content = document.getElementById(`content-${t}`);
if (t === tabName) {
btn.classList.remove('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300', 'dark:text-gray-400', 'dark:hover:text-gray-300');
btn.classList.add('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
content.classList.remove('hidden');
content.classList.add('block');
} else {
btn.classList.add('border-transparent', 'text-gray-500', 'hover:text-gray-700', 'hover:border-gray-300', 'dark:text-gray-400', 'dark:hover:text-gray-300');
btn.classList.remove('border-blue-500', 'text-blue-600', 'dark:text-blue-400');
content.classList.remove('block');
content.classList.add('hidden');
}
});
// Resize editors to ensure they render correctly after visibility change
if (tabName === 'code') codeEditor.resize();
if (tabName === 'env') envEditor.resize();
}
// Initialize Ace Editors
var codeEditor = ace.edit("code-editor");
codeEditor.setTheme("ace/theme/github_dark");
codeEditor.session.setMode("ace/mode/{{ 'python' if function.runtime == 'python' else 'javascript' }}");
codeEditor.setValue({{ function.script_content | tojson | safe }}, -1);
codeEditor.setReadOnly(true);
codeEditor.setOptions({
fontSize: "14px",
showPrintMargin: false,
highlightActiveLine: false,
highlightGutterLine: false
});
var envEditor = ace.edit("env-editor");
envEditor.setTheme("ace/theme/github_dark");
envEditor.session.setMode("ace/mode/json");
envEditor.setValue({{ function.environment_info | safe }}, -1);
envEditor.setReadOnly(true);
envEditor.setOptions({
fontSize: "14px",
showPrintMargin: false,
highlightActiveLine: false,
highlightGutterLine: false
});
</script>
{% endblock %}

View File

@@ -63,6 +63,17 @@
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </svg>
Settings Settings
</a><a
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer"
data-id="18" href="{{ url_for('community.index') }}">
<svg data-slot="icon" data-darkreader-inline-stroke="" width="18" height="18" fill="none"
stroke-width="1.5" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round"
d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z">
</path>
</svg>
Community
</a></nav> </a></nav>
</div> </div>

View File

@@ -25,6 +25,7 @@ history_url=url_for('http.history', function_id=function_id)) }}
name: '{{ name }}', name: '{{ name }}',
path: '{{ path }}', path: '{{ path }}',
functionId: {{ id }}, functionId: {{ id }},
description: '{{ description }}',
jsValue: {{ script_content | tojson | safe }}, jsValue: {{ script_content | tojson | safe }},
jsonValue: {{ environment_info | tojson | safe }}, jsonValue: {{ environment_info | tojson | safe }},
isEdit: true, isEdit: true,