Add community section where public functions can be viewed
This commit is contained in:
2
app.py
2
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
|
||||
|
||||
68
db.py
68
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(
|
||||
|
||||
50
routes/community.py
Normal file
50
routes/community.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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(
|
||||
|
||||
70
templates/community/index.html
Normal file
70
templates/community/index.html
Normal 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 %}
|
||||
152
templates/community/view.html
Normal file
152
templates/community/view.html
Normal 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 %}
|
||||
@@ -63,6 +63,17 @@
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user