Compare commits
10 Commits
d04b7f2120
...
717a18fa3c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
717a18fa3c | ||
|
|
049c875bc2 | ||
|
|
c5eb1ce463 | ||
|
|
54dbcb45fa | ||
|
|
9311893c57 | ||
|
|
46339cc4cf | ||
|
|
2253c8f7a7 | ||
|
|
b863a5a9ae | ||
|
|
1dd9040d24 | ||
|
|
71296b1301 |
9
app.py
9
app.py
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from flask import Flask, Response, jsonify, redirect, render_template, render_template_string, request, url_for
|
from flask import Flask, Response, jsonify, redirect, render_template, render_template_string, request, url_for
|
||||||
import jinja_partials
|
import jinja_partials
|
||||||
from jinja2_fragments import render_block
|
from jinja2_fragments import render_block
|
||||||
@@ -19,6 +20,7 @@ from routes.auth import auth
|
|||||||
from routes.settings import settings
|
from routes.settings import settings
|
||||||
from routes.community import community
|
from routes.community import community
|
||||||
from routes.shared_env import shared_env
|
from routes.shared_env import shared_env
|
||||||
|
from routes.tests import tests_bp
|
||||||
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
|
||||||
@@ -49,6 +51,7 @@ 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')
|
app.register_blueprint(community, url_prefix='/community')
|
||||||
app.register_blueprint(shared_env, url_prefix='/shared_env')
|
app.register_blueprint(shared_env, url_prefix='/shared_env')
|
||||||
|
app.register_blueprint(tests_bp, url_prefix='/tests')
|
||||||
|
|
||||||
# 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
|
||||||
@@ -129,6 +132,7 @@ async def execute_code():
|
|||||||
@app.route('/f/<int:user_id>/<path:function>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
|
@app.route('/f/<int:user_id>/<path:function>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'])
|
||||||
async def execute_http_function(user_id, function):
|
async def execute_http_function(user_id, function):
|
||||||
try:
|
try:
|
||||||
|
start_time = time.time()
|
||||||
# Split the function_path into the function name and the sub-path
|
# Split the function_path into the function name and the sub-path
|
||||||
parts = function.split('/', 1)
|
parts = function.split('/', 1)
|
||||||
function_name = parts[0]
|
function_name = parts[0]
|
||||||
@@ -267,6 +271,9 @@ async def execute_http_function(user_id, function):
|
|||||||
# Update function's own environment (without shared envs)
|
# Update function's own environment (without shared envs)
|
||||||
db.update_http_function_environment_info_and_invoked_count(user_id, function_name, function_specific_env)
|
db.update_http_function_environment_info_and_invoked_count(user_id, function_name, function_specific_env)
|
||||||
|
|
||||||
|
# Calculate execution time in milliseconds
|
||||||
|
execution_time = (time.time() - start_time) * 1000
|
||||||
|
|
||||||
db.add_http_function_invocation(
|
db.add_http_function_invocation(
|
||||||
http_function['id'],
|
http_function['id'],
|
||||||
response_data['status'],
|
response_data['status'],
|
||||||
@@ -274,7 +281,7 @@ async def execute_http_function(user_id, function):
|
|||||||
response_data['result'] if (log_response or response_data['status'] != 'SUCCESS') else {},
|
response_data['result'] if (log_response or response_data['status'] != 'SUCCESS') else {},
|
||||||
response_data['logs'],
|
response_data['logs'],
|
||||||
version_number,
|
version_number,
|
||||||
response_data.get('execution_time'))
|
execution_time)
|
||||||
|
|
||||||
if response_data['status'] != 'SUCCESS':
|
if response_data['status'] != 'SUCCESS':
|
||||||
return render_template("function_error.html", function_name=function_name ,error=response_data['result'], logs=response_data['logs'])
|
return render_template("function_error.html", function_name=function_name ,error=response_data['result'], logs=response_data['logs'])
|
||||||
|
|||||||
59
db.py
59
db.py
@@ -184,7 +184,7 @@ ORDER BY invocation_time DESC""", [http_function_id])
|
|||||||
'UPDATE users SET theme_preference=%s WHERE id=%s', [theme, user_id], commit=True)
|
'UPDATE users SET theme_preference=%s WHERE id=%s', [theme, user_id], commit=True)
|
||||||
def get_http_function_history(self, function_id):
|
def get_http_function_history(self, function_id):
|
||||||
http_function_history = self.execute(
|
http_function_history = self.execute(
|
||||||
'SELECT version_id, http_function_id, script_content, version_number, updated_at FROM http_functions_versions WHERE http_function_id=%s ORDER BY version_number DESC', [function_id])
|
'SELECT version_id, http_function_id, script_content, version_number, updated_at, commit_message FROM http_functions_versions WHERE http_function_id=%s ORDER BY version_number DESC', [function_id])
|
||||||
return http_function_history
|
return http_function_history
|
||||||
|
|
||||||
def create_api_key(self, user_id, name, key, scopes, rate_limit_count=None, rate_limit_period=None):
|
def create_api_key(self, user_id, name, key, scopes, rate_limit_count=None, rate_limit_period=None):
|
||||||
@@ -640,3 +640,60 @@ ORDER BY invocation_time DESC""", [http_function_id])
|
|||||||
[user_id],
|
[user_id],
|
||||||
commit=True
|
commit=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Function Testing Methods
|
||||||
|
def create_function_test(self, http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions=None):
|
||||||
|
"""Create a new test case for a function"""
|
||||||
|
test = self.execute(
|
||||||
|
'''INSERT INTO http_function_tests
|
||||||
|
(http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
|
RETURNING id, http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions, created_at, updated_at''',
|
||||||
|
[http_function_id, name, description, request_method, json.dumps(request_headers), json.dumps(request_body), expected_status, json.dumps(expected_output) if expected_output else None, json.dumps(assertions) if assertions else '[]'],
|
||||||
|
commit=True,
|
||||||
|
one=True
|
||||||
|
)
|
||||||
|
return test
|
||||||
|
|
||||||
|
def get_function_tests(self, http_function_id):
|
||||||
|
"""Get all test cases for a function"""
|
||||||
|
tests = self.execute(
|
||||||
|
'''SELECT id, http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions, created_at, updated_at
|
||||||
|
FROM http_function_tests
|
||||||
|
WHERE http_function_id = %s
|
||||||
|
ORDER BY created_at DESC''',
|
||||||
|
[http_function_id]
|
||||||
|
)
|
||||||
|
return tests if tests else []
|
||||||
|
|
||||||
|
def get_function_test(self, test_id):
|
||||||
|
"""Get a single test case"""
|
||||||
|
test = self.execute(
|
||||||
|
'''SELECT id, http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions, created_at, updated_at
|
||||||
|
FROM http_function_tests
|
||||||
|
WHERE id = %s''',
|
||||||
|
[test_id],
|
||||||
|
one=True
|
||||||
|
)
|
||||||
|
return test
|
||||||
|
|
||||||
|
def update_function_test(self, test_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions=None):
|
||||||
|
"""Update an existing test case"""
|
||||||
|
test = self.execute(
|
||||||
|
'''UPDATE http_function_tests
|
||||||
|
SET name = %s, description = %s, request_method = %s, request_headers = %s, request_body = %s, expected_status = %s, expected_output = %s, assertions = %s, updated_at = NOW()
|
||||||
|
WHERE id = %s
|
||||||
|
RETURNING id, http_function_id, name, description, request_method, request_headers, request_body, expected_status, expected_output, assertions, created_at, updated_at''',
|
||||||
|
[name, description, request_method, json.dumps(request_headers), json.dumps(request_body), expected_status, json.dumps(expected_output) if expected_output else None, json.dumps(assertions) if assertions else '[]', test_id],
|
||||||
|
commit=True,
|
||||||
|
one=True
|
||||||
|
)
|
||||||
|
return test
|
||||||
|
|
||||||
|
def delete_function_test(self, test_id):
|
||||||
|
"""Delete a test case"""
|
||||||
|
self.execute(
|
||||||
|
'DELETE FROM http_function_tests WHERE id = %s',
|
||||||
|
[test_id],
|
||||||
|
commit=True
|
||||||
|
)
|
||||||
|
|||||||
5
migrations/006_add_commit_messages.sql
Normal file
5
migrations/006_add_commit_messages.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE http_functions_versions
|
||||||
|
ADD COLUMN IF NOT EXISTS commit_message TEXT;
|
||||||
|
|
||||||
|
ALTER TABLE timer_function_versions
|
||||||
|
ADD COLUMN IF NOT EXISTS commit_message TEXT;
|
||||||
15
migrations/007_create_function_tests.sql
Normal file
15
migrations/007_create_function_tests.sql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS http_function_tests (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
http_function_id INTEGER NOT NULL REFERENCES http_functions(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
request_method VARCHAR(10) DEFAULT 'POST',
|
||||||
|
request_headers JSONB DEFAULT '{}',
|
||||||
|
request_body JSONB DEFAULT '{}',
|
||||||
|
expected_status VARCHAR(50) NOT NULL DEFAULT 'SUCCESS',
|
||||||
|
expected_output JSONB,
|
||||||
|
created_at TIMESTAMP DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_http_function_tests_function_id ON http_function_tests(http_function_id);
|
||||||
2
migrations/008_add_test_assertions.sql
Normal file
2
migrations/008_add_test_assertions.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE http_function_tests
|
||||||
|
ADD COLUMN IF NOT EXISTS assertions JSONB DEFAULT '[]';
|
||||||
@@ -222,6 +222,7 @@ def edit(function_id):
|
|||||||
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", "")
|
description = request.json.get("description", "")
|
||||||
|
commit_message = request.json.get("commit_message", "")
|
||||||
|
|
||||||
updated_version = db.edit_http_function(
|
updated_version = db.edit_http_function(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -237,6 +238,22 @@ def edit(function_id):
|
|||||||
description
|
description
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update the commit message for the newly created version
|
||||||
|
# Note: The database trigger creates a new version after the UPDATE,
|
||||||
|
# so we need to get the latest version number
|
||||||
|
if commit_message:
|
||||||
|
latest_version = db.execute(
|
||||||
|
"SELECT MAX(version_number) as version_number FROM http_functions_versions WHERE http_function_id = %s",
|
||||||
|
[function_id],
|
||||||
|
one=True
|
||||||
|
)
|
||||||
|
if latest_version:
|
||||||
|
db.execute(
|
||||||
|
"UPDATE http_functions_versions SET commit_message = %s WHERE http_function_id = %s AND version_number = %s",
|
||||||
|
[commit_message, function_id, latest_version['version_number']],
|
||||||
|
commit=True
|
||||||
|
)
|
||||||
|
|
||||||
return {"status": "success", "message": f"{name} updated"}
|
return {"status": "success", "message": f"{name} updated"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
@@ -331,7 +348,7 @@ def history(function_id):
|
|||||||
# Fetch all versions
|
# Fetch all versions
|
||||||
versions = db.execute(
|
versions = db.execute(
|
||||||
"""
|
"""
|
||||||
SELECT version_number, script_content AS script, updated_at AS versioned_at
|
SELECT version_number, script_content AS script, updated_at AS versioned_at, commit_message
|
||||||
FROM http_functions_versions
|
FROM http_functions_versions
|
||||||
WHERE http_function_id = %s
|
WHERE http_function_id = %s
|
||||||
ORDER BY version_number DESC
|
ORDER BY version_number DESC
|
||||||
@@ -439,3 +456,30 @@ def restore(function_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
return jsonify({"status": "error", "message": str(e)}), 500
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@http.route("/tests/<int:function_id>", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def tests(function_id):
|
||||||
|
"""Render the tests page for a function"""
|
||||||
|
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"]
|
||||||
|
|
||||||
|
if htmx:
|
||||||
|
return render_block(
|
||||||
|
environment,
|
||||||
|
"dashboard/http_functions/tests.html",
|
||||||
|
"page",
|
||||||
|
function_id=function_id,
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"dashboard/http_functions/tests.html",
|
||||||
|
function_id=function_id,
|
||||||
|
name=name
|
||||||
|
)
|
||||||
|
|||||||
324
routes/tests.py
Normal file
324
routes/tests.py
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from flask_login import current_user, login_required
|
||||||
|
from extensions import db
|
||||||
|
import json
|
||||||
|
|
||||||
|
tests_bp = Blueprint('tests', __name__)
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/<int:function_id>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def list_tests(function_id):
|
||||||
|
"""Get all test cases for a function"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Verify function ownership
|
||||||
|
http_function = db.get_http_function_by_id(user_id, function_id)
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Function not found'}), 404
|
||||||
|
|
||||||
|
tests = db.get_function_tests(function_id)
|
||||||
|
return jsonify({'tests': tests})
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/<int:function_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def create_test(function_id):
|
||||||
|
"""Create a new test case"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Verify function ownership
|
||||||
|
http_function = db.get_http_function_by_id(user_id, function_id)
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Function not found'}), 404
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
name = data.get('name')
|
||||||
|
description = data.get('description', '')
|
||||||
|
request_method = data.get('request_method', 'POST')
|
||||||
|
request_headers = data.get('request_headers', {})
|
||||||
|
request_body = data.get('request_body', {})
|
||||||
|
expected_status = data.get('expected_status', 'SUCCESS')
|
||||||
|
expected_output = data.get('expected_output')
|
||||||
|
assertions = data.get('assertions', [])
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
return jsonify({'error': 'Test name is required'}), 400
|
||||||
|
|
||||||
|
test = db.create_function_test(
|
||||||
|
function_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
request_method,
|
||||||
|
request_headers,
|
||||||
|
request_body,
|
||||||
|
expected_status,
|
||||||
|
expected_output,
|
||||||
|
assertions
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'test': test})
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/test/<int:test_id>', methods=['PUT'])
|
||||||
|
@login_required
|
||||||
|
def update_test(test_id):
|
||||||
|
"""Update a test case"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Get and verify test ownership
|
||||||
|
test = db.get_function_test(test_id)
|
||||||
|
if not test:
|
||||||
|
return jsonify({'error': 'Test not found'}), 404
|
||||||
|
|
||||||
|
http_function = db.get_http_function_by_id(user_id, test['http_function_id'])
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
updated_test = db.update_function_test(
|
||||||
|
test_id,
|
||||||
|
data.get('name', test['name']),
|
||||||
|
data.get('description', test['description']),
|
||||||
|
data.get('request_method', test['request_method']),
|
||||||
|
data.get('request_headers', test['request_headers']),
|
||||||
|
data.get('request_body', test['request_body']),
|
||||||
|
data.get('expected_status', test['expected_status']),
|
||||||
|
data.get('expected_output', test['expected_output']),
|
||||||
|
data.get('assertions', test.get('assertions', []))
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({'status': 'success', 'test': updated_test})
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/test/<int:test_id>', methods=['DELETE'])
|
||||||
|
@login_required
|
||||||
|
def delete_test(test_id):
|
||||||
|
"""Delete a test case"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Get and verify test ownership
|
||||||
|
test = db.get_function_test(test_id)
|
||||||
|
if not test:
|
||||||
|
return jsonify({'error': 'Test not found'}), 404
|
||||||
|
|
||||||
|
http_function = db.get_http_function_by_id(user_id, test['http_function_id'])
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
db.delete_function_test(test_id)
|
||||||
|
return jsonify({'status': 'success', 'message': 'Test deleted'})
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/run/<int:test_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def run_single_test(test_id):
|
||||||
|
"""Run a single test case"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Get and verify test ownership
|
||||||
|
test = db.get_function_test(test_id)
|
||||||
|
if not test:
|
||||||
|
return jsonify({'error': 'Test not found'}), 404
|
||||||
|
|
||||||
|
http_function = db.get_http_function_by_id(user_id, test['http_function_id'])
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 403
|
||||||
|
|
||||||
|
# Execute the function with test input
|
||||||
|
result = execute_test(http_function, test)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
@tests_bp.route('/http/tests/run-all/<int:function_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def run_all_tests(function_id):
|
||||||
|
"""Run all tests for a function"""
|
||||||
|
user_id = current_user.id
|
||||||
|
|
||||||
|
# Verify function ownership
|
||||||
|
http_function = db.get_http_function_by_id(user_id, function_id)
|
||||||
|
if not http_function:
|
||||||
|
return jsonify({'error': 'Function not found'}), 404
|
||||||
|
|
||||||
|
tests = db.get_function_tests(function_id)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
result = execute_test(http_function, test)
|
||||||
|
results.append({
|
||||||
|
'test_id': test['id'],
|
||||||
|
'test_name': test['name'],
|
||||||
|
**result
|
||||||
|
})
|
||||||
|
|
||||||
|
passed_count = sum(1 for r in results if r['passed'])
|
||||||
|
total_count = len(results)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'results': results,
|
||||||
|
'summary': {
|
||||||
|
'total': total_count,
|
||||||
|
'passed': passed_count,
|
||||||
|
'failed': total_count - passed_count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def get_nested_value(data, path):
|
||||||
|
"""Get value from nested dict using dot notation (e.g., 'user.email')"""
|
||||||
|
if not path:
|
||||||
|
return data
|
||||||
|
|
||||||
|
keys = path.split('.')
|
||||||
|
value = data
|
||||||
|
try:
|
||||||
|
for key in keys:
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = value.get(key)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def evaluate_assertion(assertion, response_data):
|
||||||
|
"""Evaluate a single assertion against response data"""
|
||||||
|
assertion_type = assertion.get('type')
|
||||||
|
path = assertion.get('path', '')
|
||||||
|
description = assertion.get('description', '')
|
||||||
|
|
||||||
|
# Get value at path
|
||||||
|
value = get_nested_value(response_data, path)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'type': assertion_type,
|
||||||
|
'description': description,
|
||||||
|
'path': path,
|
||||||
|
'passed': False,
|
||||||
|
'message': ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if assertion_type == 'field_exists':
|
||||||
|
result['passed'] = value is not None
|
||||||
|
result['message'] = f"Field '{path}' {'exists' if result['passed'] else 'does not exist'}"
|
||||||
|
|
||||||
|
elif assertion_type == 'field_equals':
|
||||||
|
expected_value = assertion.get('value')
|
||||||
|
result['passed'] = value == expected_value
|
||||||
|
result['expected'] = expected_value
|
||||||
|
result['actual'] = value
|
||||||
|
result['message'] = f"Expected {expected_value} but got {value}" if not result['passed'] else f"Field '{path}' equals {expected_value}"
|
||||||
|
|
||||||
|
elif assertion_type == 'field_type':
|
||||||
|
expected_type = assertion.get('expectedType')
|
||||||
|
actual_type = type(value).__name__ if value is not None else 'None'
|
||||||
|
result['passed'] = actual_type == expected_type
|
||||||
|
result['expected'] = expected_type
|
||||||
|
result['actual'] = actual_type
|
||||||
|
result['message'] = f"Expected type {expected_type} but got {actual_type}" if not result['passed'] else f"Field '{path}' is {expected_type}"
|
||||||
|
|
||||||
|
elif assertion_type == 'field_contains':
|
||||||
|
substring = assertion.get('substring', '')
|
||||||
|
if isinstance(value, str):
|
||||||
|
result['passed'] = substring in value
|
||||||
|
result['message'] = f"Field '{path}' {'contains' if result['passed'] else 'does not contain'} '{substring}'"
|
||||||
|
else:
|
||||||
|
result['passed'] = False
|
||||||
|
result['message'] = f"Field '{path}' is not a string"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def execute_test(http_function, test):
|
||||||
|
"""Execute a test case and return the result"""
|
||||||
|
import requests
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Prepare request data
|
||||||
|
request_data = {
|
||||||
|
'code': http_function['script_content'],
|
||||||
|
'environment': http_function['environment_info'] if isinstance(http_function['environment_info'], dict) else json.loads(http_function['environment_info']),
|
||||||
|
'request': {
|
||||||
|
'method': test['request_method'],
|
||||||
|
'headers': test['request_headers'] if isinstance(test['request_headers'], dict) else json.loads(test['request_headers']),
|
||||||
|
'json': test['request_body'] if isinstance(test['request_body'], dict) else json.loads(test['request_body']),
|
||||||
|
'url': f'/test/{http_function["name"]}',
|
||||||
|
'path': '/'
|
||||||
|
},
|
||||||
|
'name': http_function['name']
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call the appropriate runtime isolator
|
||||||
|
runtime = http_function.get('runtime', 'node')
|
||||||
|
if runtime == 'python':
|
||||||
|
api_url = 'http://python-isolator.web:5000/execute'
|
||||||
|
elif runtime == 'deno':
|
||||||
|
api_url = 'http://deno-isolator.web:5000/execute'
|
||||||
|
else:
|
||||||
|
api_url = 'http://isolator.web:5000/execute'
|
||||||
|
|
||||||
|
response = requests.post(api_url, json=request_data, timeout=30)
|
||||||
|
response_data = response.json()
|
||||||
|
|
||||||
|
# Get actual results
|
||||||
|
actual_status = response_data.get('status')
|
||||||
|
actual_output = response_data.get('result')
|
||||||
|
expected_status = test['expected_status']
|
||||||
|
|
||||||
|
# Get assertions
|
||||||
|
assertions = test.get('assertions')
|
||||||
|
if isinstance(assertions, str):
|
||||||
|
assertions = json.loads(assertions) if assertions else []
|
||||||
|
elif assertions is None:
|
||||||
|
assertions = []
|
||||||
|
|
||||||
|
# If assertions exist, use assertion-based validation
|
||||||
|
if assertions and len(assertions) > 0:
|
||||||
|
assertion_results = []
|
||||||
|
for idx, assertion in enumerate(assertions):
|
||||||
|
assertion_result = evaluate_assertion(assertion, actual_output)
|
||||||
|
assertion_result['id'] = idx
|
||||||
|
assertion_results.append(assertion_result)
|
||||||
|
|
||||||
|
# Test passes if all assertions pass
|
||||||
|
passed = all(a['passed'] for a in assertion_results)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'passed': passed,
|
||||||
|
'actual_status': actual_status,
|
||||||
|
'actual_output': actual_output,
|
||||||
|
'expected_status': expected_status,
|
||||||
|
'assertions': assertion_results,
|
||||||
|
'logs': response_data.get('logs', []),
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fallback to old expected_output comparison
|
||||||
|
else:
|
||||||
|
expected_output = test['expected_output'] if isinstance(test['expected_output'], dict) else (json.loads(test['expected_output']) if test['expected_output'] else None)
|
||||||
|
|
||||||
|
# Check if test passed
|
||||||
|
status_match = actual_status == expected_status
|
||||||
|
output_match = True
|
||||||
|
|
||||||
|
# If expected output is specified, compare it
|
||||||
|
if expected_output is not None:
|
||||||
|
output_match = actual_output == expected_output
|
||||||
|
|
||||||
|
passed = status_match and output_match
|
||||||
|
|
||||||
|
return {
|
||||||
|
'passed': passed,
|
||||||
|
'actual_status': actual_status,
|
||||||
|
'actual_output': actual_output,
|
||||||
|
'expected_status': expected_status,
|
||||||
|
'expected_output': expected_output,
|
||||||
|
'logs': response_data.get('logs', []),
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'passed': False,
|
||||||
|
'actual_status': 'ERROR',
|
||||||
|
'actual_output': None,
|
||||||
|
'expected_status': test['expected_status'],
|
||||||
|
'expected_output': test.get('expected_output'),
|
||||||
|
'logs': [],
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
@@ -354,6 +354,7 @@ def edit(function_id):
|
|||||||
data = request.json
|
data = request.json
|
||||||
trigger_type = data.get('trigger_type')
|
trigger_type = data.get('trigger_type')
|
||||||
runtime = data.get('runtime', 'node')
|
runtime = data.get('runtime', 'node')
|
||||||
|
commit_message = data.get('commit_message', '')
|
||||||
|
|
||||||
# Validate trigger type
|
# Validate trigger type
|
||||||
if trigger_type not in ('interval', 'date', 'cron'):
|
if trigger_type not in ('interval', 'date', 'cron'):
|
||||||
@@ -416,6 +417,22 @@ def edit(function_id):
|
|||||||
],
|
],
|
||||||
commit=True)
|
commit=True)
|
||||||
|
|
||||||
|
# Update the commit message for the newly created version
|
||||||
|
# Note: The database trigger creates a new version after the UPDATE,
|
||||||
|
# so we need to get the latest version number
|
||||||
|
if commit_message:
|
||||||
|
latest_version = db.execute(
|
||||||
|
"SELECT MAX(version_number) as version_number FROM timer_function_versions WHERE timer_function_id = %s",
|
||||||
|
[function_id],
|
||||||
|
one=True
|
||||||
|
)
|
||||||
|
if latest_version:
|
||||||
|
db.execute(
|
||||||
|
"UPDATE timer_function_versions SET commit_message = %s WHERE timer_function_id = %s AND version_number = %s",
|
||||||
|
[commit_message, function_id, latest_version['version_number']],
|
||||||
|
commit=True
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": "Timer function updated successfully"
|
"message": "Timer function updated successfully"
|
||||||
@@ -572,7 +589,7 @@ def history(function_id):
|
|||||||
|
|
||||||
# Fetch all versions
|
# Fetch all versions
|
||||||
versions = db.execute("""
|
versions = db.execute("""
|
||||||
SELECT version_number, script, versioned_at
|
SELECT version_number, script, versioned_at, commit_message
|
||||||
FROM timer_function_versions
|
FROM timer_function_versions
|
||||||
WHERE timer_function_id = %s
|
WHERE timer_function_id = %s
|
||||||
ORDER BY version_number DESC
|
ORDER BY version_number DESC
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ const FunctionHistory = {
|
|||||||
"div.text-sm.text-gray-600.dark:text-gray-400",
|
"div.text-sm.text-gray-600.dark:text-gray-400",
|
||||||
new Date(version.versioned_at).toLocaleString()
|
new Date(version.versioned_at).toLocaleString()
|
||||||
),
|
),
|
||||||
|
version.commit_message && m(
|
||||||
|
"div.text-xs.text-gray-500.dark:text-gray-500.italic.mt-1",
|
||||||
|
version.commit_message
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
430
static/js/mithril/TestRunner.js
Normal file
430
static/js/mithril/TestRunner.js
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
const TestRunner = {
|
||||||
|
oninit(vnode) {
|
||||||
|
this.function_id = vnode.attrs.function_id;
|
||||||
|
this.function_name = vnode.attrs.function_name;
|
||||||
|
this.tests = [];
|
||||||
|
this.testResults = {};
|
||||||
|
this.loading = true;
|
||||||
|
this.runningTests = false;
|
||||||
|
this.showCreateModal = false;
|
||||||
|
this.selectedTestForEdit = null;
|
||||||
|
|
||||||
|
// New test form data
|
||||||
|
this.newTest = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
request_method: 'POST',
|
||||||
|
request_headers: '{}',
|
||||||
|
request_body: '{}',
|
||||||
|
expected_status: 'SUCCESS',
|
||||||
|
expected_output: '{}',
|
||||||
|
assertions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load tests
|
||||||
|
this.loadTests();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadTests() {
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const response = await m.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/tests/http/tests/${this.function_id}`
|
||||||
|
});
|
||||||
|
this.tests = response.tests || [];
|
||||||
|
} catch (err) {
|
||||||
|
Alert.show('Error loading tests: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTest() {
|
||||||
|
try {
|
||||||
|
const response = await m.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/tests/http/tests/${this.function_id}`,
|
||||||
|
body: {
|
||||||
|
name: this.newTest.name,
|
||||||
|
description: this.newTest.description,
|
||||||
|
request_method: this.newTest.request_method,
|
||||||
|
request_headers: JSON.parse(this.newTest.request_headers),
|
||||||
|
request_body: JSON.parse(this.newTest.request_body),
|
||||||
|
expected_status: this.newTest.expected_status,
|
||||||
|
expected_output: this.newTest.expected_output ? JSON.parse(this.newTest.expected_output) : null,
|
||||||
|
assertions: this.newTest.assertions
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.show('Test created successfully', 'success');
|
||||||
|
this.showCreateModal = false;
|
||||||
|
this.resetNewTestForm();
|
||||||
|
await this.loadTests();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.show('Error creating test: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTest(testId) {
|
||||||
|
if (!confirm('Are you sure you want to delete this test?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await m.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/tests/http/tests/test/${testId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.show('Test deleted', 'success');
|
||||||
|
await this.loadTests();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.show('Error deleting test: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async runSingleTest(testId) {
|
||||||
|
try {
|
||||||
|
this.runningTests = true;
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
const result = await m.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/tests/http/tests/run/${testId}`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.testResults[testId] = result;
|
||||||
|
m.redraw();
|
||||||
|
} catch (err) {
|
||||||
|
Alert.show('Error running test: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
this.runningTests = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async runAllTests() {
|
||||||
|
try {
|
||||||
|
this.runningTests = true;
|
||||||
|
this.testResults = {};
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
const response = await m.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/tests/http/tests/run-all/${this.function_id}`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map results by test_id
|
||||||
|
response.results.forEach(result => {
|
||||||
|
this.testResults[result.test_id] = result;
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = response.summary;
|
||||||
|
Alert.show(`Tests completed: ${summary.passed}/${summary.total} passed`, summary.passed === summary.total ? 'success' : 'error');
|
||||||
|
} catch (err) {
|
||||||
|
Alert.show('Error running tests: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
this.runningTests = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetNewTestForm() {
|
||||||
|
this.newTest = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
request_method: 'POST',
|
||||||
|
request_headers: '{}',
|
||||||
|
request_body: '{}',
|
||||||
|
expected_status: 'SUCCESS',
|
||||||
|
expected_output: '{}',
|
||||||
|
assertions: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addAssertion() {
|
||||||
|
this.newTest.assertions.push({
|
||||||
|
type: 'field_exists',
|
||||||
|
path: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAssertion(index) {
|
||||||
|
this.newTest.assertions.splice(index, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
view(vnode) {
|
||||||
|
const passedCount = Object.values(this.testResults).filter(r => r.passed).length;
|
||||||
|
const totalTests = this.tests.length;
|
||||||
|
const hasResults = Object.keys(this.testResults).length > 0;
|
||||||
|
|
||||||
|
return m('div.p-6', [
|
||||||
|
// Header
|
||||||
|
m('div.flex.justify-between.items-center.mb-6', [
|
||||||
|
m('div', [
|
||||||
|
m('h2.text-2xl.font-bold.text-gray-900.dark:text-white', `Tests for ${this.function_name}`),
|
||||||
|
hasResults && m('p.text-sm.text-gray-600.dark:text-gray-400.mt-1',
|
||||||
|
`${passedCount}/${Object.keys(this.testResults).length} tests passed`
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
m('div.flex.gap-2', [
|
||||||
|
m('button.px-4.py-2.bg-blue-500.text-white.rounded-lg.hover:bg-blue-600.disabled:opacity-50', {
|
||||||
|
onclick: () => this.showCreateModal = true
|
||||||
|
}, '+ New Test'),
|
||||||
|
m('button.px-4.py-2.bg-green-500.text-white.rounded-lg.hover:bg-green-600.disabled:opacity-50', {
|
||||||
|
onclick: () => this.runAllTests(),
|
||||||
|
disabled: this.runningTests || this.tests.length === 0
|
||||||
|
}, this.runningTests ? 'Running...' : '▶ Run All Tests')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Tests List
|
||||||
|
this.loading
|
||||||
|
? m('div.text-center.py-12', m('div.animate-spin.h-8.w-8.border-4.border-blue-500.border-t-transparent.rounded-full.mx-auto'))
|
||||||
|
: this.tests.length === 0
|
||||||
|
? m('div.text-center.py-12', [
|
||||||
|
m('p.text-gray-600.dark:text-gray-400', 'No tests yet'),
|
||||||
|
m('p.text-sm.text-gray-500.dark:text-gray-500.mt-2', 'Create your first test to ensure your function works as expected')
|
||||||
|
])
|
||||||
|
: m('div.space-y-3', this.tests.map(test => {
|
||||||
|
const result = this.testResults[test.id];
|
||||||
|
const isPassed = result?.passed === true;
|
||||||
|
const isFailed = result?.passed === false;
|
||||||
|
|
||||||
|
return m('div.bg-white.dark:bg-gray-800.border.rounded-lg.p-4', {
|
||||||
|
class: isPassed ? 'border-green-500' : isFailed ? 'border-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
|
}, [
|
||||||
|
m('div.flex.justify-between.items-start', [
|
||||||
|
m('div.flex-1', [
|
||||||
|
m('div.flex.items-center.gap-2', [
|
||||||
|
result && m('span.text-2xl', isPassed ? '✓' : '✗'),
|
||||||
|
m('h3.text-lg.font-semibold.text-gray-900.dark:text-white', test.name),
|
||||||
|
m('span.text-xs.px-2.py-1.rounded.bg-gray-100.dark:bg-gray-700', test.request_method)
|
||||||
|
]),
|
||||||
|
test.description && m('p.text-sm.text-gray-600.dark:text-gray-400.mt-1', test.description),
|
||||||
|
|
||||||
|
// Test Results
|
||||||
|
result && m('div.mt-3.space-y-2', [
|
||||||
|
// Show assertion results if they exist
|
||||||
|
result.assertions && result.assertions.length > 0 ? [
|
||||||
|
m('div.text-sm.font-medium.mb-2', `Assertions: ${result.assertions.filter(a => a.passed).length}/${result.assertions.length} passed`),
|
||||||
|
result.assertions.map(assertion =>
|
||||||
|
m('div.text-xs.p-2.rounded.border', {
|
||||||
|
class: assertion.passed
|
||||||
|
? 'bg-green-50.dark:bg-green-900/20.border-green-200.dark:border-green-800'
|
||||||
|
: 'bg-red-50.dark:bg-red-900/20.border-red-200.dark:border-red-800'
|
||||||
|
}, [
|
||||||
|
m('div.flex.items-center.gap-2', [
|
||||||
|
m('span', assertion.passed ? '✓' : '✗'),
|
||||||
|
m('span.font-medium', assertion.description || `${assertion.type}: ${assertion.path}`)
|
||||||
|
]),
|
||||||
|
m('p.text-gray-600.dark:text-gray-400.mt-1', assertion.message)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
] : [
|
||||||
|
// Fallback to old output comparison
|
||||||
|
isFailed && m('div.bg-red-50.dark:bg-red-900/20.border.border-red-200.dark:border-red-800.rounded.p-3', [
|
||||||
|
m('p.text-sm.font-medium.text-red-800.dark:text-red-300', 'Test Failed'),
|
||||||
|
result.error && m('p.text-xs.text-red-600.dark:text-red-400.mt-1', `Error: ${result.error}`),
|
||||||
|
result.actual_output !== result.expected_output && m('div.mt-2', [
|
||||||
|
m('div.grid.grid-cols-2.gap-2.text-xs', [
|
||||||
|
m('div', [
|
||||||
|
m('p.font-medium.text-gray-700.dark:text-gray-300', 'Expected:'),
|
||||||
|
m('pre.bg-white.dark:bg-gray-900.p-2.rounded.mt-1.overflow-auto',
|
||||||
|
JSON.stringify(result.expected_output, null, 2))
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('p.font-medium.text-gray-700.dark:text-gray-300', 'Actual:'),
|
||||||
|
m('pre.bg-white.dark:bg-gray-900.p-2.rounded.mt-1.overflow-auto',
|
||||||
|
JSON.stringify(result.actual_output, null, 2))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
isPassed && m('div.bg-green-50.dark:bg-green-900/20.border.border-green-200.dark:border-green-800.rounded.p-3', [
|
||||||
|
m('p.text-sm.font-medium.text-green-800.dark:text-green-300', '✓ Test Passed')
|
||||||
|
])
|
||||||
|
]
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
m('div.flex.gap-2', [
|
||||||
|
m('button.px-3.py-1.text-sm.bg-blue-500.text-white.rounded.hover:bg-blue-600', {
|
||||||
|
onclick: () => this.runSingleTest(test.id),
|
||||||
|
disabled: this.runningTests
|
||||||
|
}, '▶ Run'),
|
||||||
|
m('button.px-3.py-1.text-sm.bg-red-500.text-white.rounded.hover:bg-red-600', {
|
||||||
|
onclick: () => this.deleteTest(test.id)
|
||||||
|
}, 'Delete')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Create Test Modal
|
||||||
|
this.showCreateModal && m('div.fixed.inset-0.bg-black.bg-opacity-50.flex.items-center.justify-center.z-50', {
|
||||||
|
onclick: () => this.showCreateModal = false
|
||||||
|
}, [
|
||||||
|
m('div.bg-white.dark:bg-gray-800.rounded-lg.p-6.max-w-2xl.w-full.overflow-y-auto', {
|
||||||
|
onclick: (e) => e.stopPropagation(),
|
||||||
|
style: 'max-height: 90vh;'
|
||||||
|
}, [
|
||||||
|
m('h3.text-xl.font-bold.mb-4.text-gray-900.dark:text-white', 'Create New Test'),
|
||||||
|
m('form', {
|
||||||
|
onsubmit: (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.createTest();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
m('div.space-y-4', [
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Test Name'),
|
||||||
|
m('input.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.name,
|
||||||
|
oninput: (e) => this.newTest.name = e.target.value,
|
||||||
|
required: true,
|
||||||
|
placeholder: 'Valid user creation'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Description'),
|
||||||
|
m('textarea.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.description,
|
||||||
|
oninput: (e) => this.newTest.description = e.target.value,
|
||||||
|
rows: 2
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m('div.grid.grid-cols-2.gap-4', [
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Method'),
|
||||||
|
m('select.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.request_method,
|
||||||
|
onchange: (e) => this.newTest.request_method = e.target.value
|
||||||
|
}, [
|
||||||
|
m('option', {value: 'GET'}, 'GET'),
|
||||||
|
m('option', {value: 'POST'}, 'POST'),
|
||||||
|
m('option', {value: 'PUT'}, 'PUT'),
|
||||||
|
m('option', {value: 'DELETE'}, 'DELETE')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Expected Status'),
|
||||||
|
m('select.w-full.p-2.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.expected_status,
|
||||||
|
onchange: (e) => this.newTest.expected_status = e.target.value
|
||||||
|
}, [
|
||||||
|
m('option', {value: 'SUCCESS'}, 'SUCCESS'),
|
||||||
|
m('option', {value: 'ERROR'}, 'ERROR')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Request Headers (JSON)'),
|
||||||
|
m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.request_headers,
|
||||||
|
oninput: (e) => this.newTest.request_headers = e.target.value,
|
||||||
|
rows: 3
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Request Body (JSON)'),
|
||||||
|
m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.request_body,
|
||||||
|
oninput: (e) => this.newTest.request_body = e.target.value,
|
||||||
|
rows: 4
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-sm.font-medium.mb-1', 'Expected Output (JSON)'),
|
||||||
|
m('textarea.w-full.p-2.border.rounded.font-mono.text-sm.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: this.newTest.expected_output,
|
||||||
|
oninput: (e) => this.newTest.expected_output = e.target.value,
|
||||||
|
rows: 4
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
// Assertions Section
|
||||||
|
m('div.border-t.pt-4.mt-4', [
|
||||||
|
m('div.flex.justify-between.items-center.mb-3', [
|
||||||
|
m('label.block.text-sm.font-medium', 'Assertions (Advanced)'),
|
||||||
|
m('button.text-sm.px-3.py-1.bg-green-500.text-white.rounded.hover:bg-green-600', {
|
||||||
|
type: 'button',
|
||||||
|
onclick: () => this.addAssertion()
|
||||||
|
}, '+ Add Assertion')
|
||||||
|
]),
|
||||||
|
this.newTest.assertions.length === 0
|
||||||
|
? m('p.text-sm.text-gray-500.dark:text-gray-400', 'No assertions yet. Add assertions to validate specific fields in the response.')
|
||||||
|
: m('div.space-y-3', this.newTest.assertions.map((assertion, idx) =>
|
||||||
|
m('div.p-3.border.rounded.dark:border-gray-600.bg-gray-50.dark:bg-gray-900', [
|
||||||
|
m('div.flex.justify-between.items-start.mb-2', [
|
||||||
|
m('span.text-xs.font-medium.text-gray-700.dark:text-gray-300', `Assertion ${idx + 1}`),
|
||||||
|
m('button.text-red-500.hover:text-red-700.text-xs', {
|
||||||
|
type: 'button',
|
||||||
|
onclick: () => this.removeAssertion(idx)
|
||||||
|
}, '✕ Remove')
|
||||||
|
]),
|
||||||
|
m('div.grid.grid-cols-2.gap-2', [
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-xs.mb-1', 'Type'),
|
||||||
|
m('select.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: assertion.type,
|
||||||
|
onchange: (e) => assertion.type = e.target.value
|
||||||
|
}, [
|
||||||
|
m('option', {value: 'field_exists'}, 'Field Exists'),
|
||||||
|
m('option', {value: 'field_equals'}, 'Field Equals'),
|
||||||
|
m('option', {value: 'field_type'}, 'Field Type'),
|
||||||
|
m('option', {value: 'field_contains'}, 'Field Contains')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
m('div', [
|
||||||
|
m('label.block.text-xs.mb-1', 'Path'),
|
||||||
|
m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: assertion.path,
|
||||||
|
oninput: (e) => assertion.path = e.target.value,
|
||||||
|
placeholder: 'e.g., user.email'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
// Conditional fields based on assertion type
|
||||||
|
(assertion.type === 'field_equals' || assertion.type === 'field_type' || assertion.type === 'field_contains') && m('div.mt-2', [
|
||||||
|
m('label.block.text-xs.mb-1',
|
||||||
|
assertion.type === 'field_equals' ? 'Expected Value' :
|
||||||
|
assertion.type === 'field_type' ? 'Expected Type' : 'Substring'),
|
||||||
|
m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: assertion.type === 'field_equals' ? assertion.value :
|
||||||
|
assertion.type === 'field_type' ? assertion.expectedType :
|
||||||
|
assertion.substring,
|
||||||
|
oninput: (e) => {
|
||||||
|
if (assertion.type === 'field_equals') assertion.value = e.target.value;
|
||||||
|
else if (assertion.type === 'field_type') assertion.expectedType = e.target.value;
|
||||||
|
else assertion.substring = e.target.value;
|
||||||
|
},
|
||||||
|
placeholder: assertion.type === 'field_type' ? 'str, int, bool, etc.' : ''
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
m('div.mt-2', [
|
||||||
|
m('label.block.text-xs.mb-1', 'Description (Optional)'),
|
||||||
|
m('input.w-full.p-1.text-sm.border.rounded.dark:bg-gray-700.dark:border-gray-600', {
|
||||||
|
value: assertion.description,
|
||||||
|
oninput: (e) => assertion.description = e.target.value,
|
||||||
|
placeholder: 'Describe this check'
|
||||||
|
})
|
||||||
|
])
|
||||||
|
])
|
||||||
|
))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
m('div.flex.justify-end.gap-2.mt-6', [
|
||||||
|
m('button.px-4.py-2.border.rounded.hover:bg-gray-100.dark:hover:bg-gray-700', {
|
||||||
|
type: 'button',
|
||||||
|
onclick: () => this.showCreateModal = false
|
||||||
|
}, 'Cancel'),
|
||||||
|
m('button.px-4.py-2.bg-blue-500.text-white.rounded.hover:bg-blue-600', {
|
||||||
|
type: 'submit'
|
||||||
|
}, 'Create Test')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -66,6 +66,9 @@ const Editor = {
|
|||||||
this.aiModalOpen = false;
|
this.aiModalOpen = false;
|
||||||
this.aiModalContent = "";
|
this.aiModalContent = "";
|
||||||
this.aiModalTitle = "";
|
this.aiModalTitle = "";
|
||||||
|
|
||||||
|
// Commit Message
|
||||||
|
this.commitMessage = "";
|
||||||
},
|
},
|
||||||
|
|
||||||
oncreate() {
|
oncreate() {
|
||||||
@@ -248,6 +251,7 @@ const Editor = {
|
|||||||
is_enabled: this.isEnabled,
|
is_enabled: this.isEnabled,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
runtime: this.runtime,
|
runtime: this.runtime,
|
||||||
|
commit_message: this.commitMessage,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -259,6 +263,7 @@ const Editor = {
|
|||||||
log_response: this.logResponse,
|
log_response: this.logResponse,
|
||||||
runtime: this.runtime,
|
runtime: this.runtime,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
|
commit_message: this.commitMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await m.request({
|
const response = await m.request({
|
||||||
@@ -946,6 +951,18 @@ const Editor = {
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Commit Message Input (only show for edit mode)
|
||||||
|
this.isEdit && m("div", { class: "flex flex-col space-y-2 pt-2" }, [
|
||||||
|
m("label", { class: "text-sm font-medium text-gray-700 dark:text-gray-300" }, "Commit Message (Optional)"),
|
||||||
|
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 the changes you made...",
|
||||||
|
value: this.commitMessage,
|
||||||
|
oninput: (e) => (this.commitMessage = e.target.value)
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
{ class: "flex items-center justify-end space-x-3 pt-2" },
|
{ class: "flex items-center justify-end space-x-3 pt-2" },
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
<script src="/static/js/mithril/alert.js"></script>
|
<script src="/static/js/mithril/alert.js"></script>
|
||||||
<script src="/static/js/mithril/diffView.js"></script>
|
<script src="/static/js/mithril/diffView.js"></script>
|
||||||
<script src="/static/js/mithril/FunctionHistory.js"></script>
|
<script src="/static/js/mithril/FunctionHistory.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/mithril/TestRunner.js') }}"></script>
|
||||||
|
|
||||||
<script src="https://unpkg.com/ace-diff@^2"></script>
|
<script src="https://unpkg.com/ace-diff@^2"></script>
|
||||||
<link href="https://unpkg.com/ace-diff@^2/dist/ace-diff.min.css" rel="stylesheet">
|
<link href="https://unpkg.com/ace-diff@^2/dist/ace-diff.min.css" rel="stylesheet">
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% set avg_time = ((stats.avg_timer_execution_time or 0) + (stats.avg_http_execution_time or 0)) / 2 %}
|
{% set avg_time = ((stats.avg_timer_execution_time or 0) + (stats.avg_http_execution_time or 0)) / 2 %}
|
||||||
<div class="flex items-baseline">
|
<div class="flex items-baseline">
|
||||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(avg_time) }}s</p>
|
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(avg_time) }}ms</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{
|
||||||
"%.2f"|format(activity.execution_time or 0) }}s</td>
|
"%.2f"|format(activity.execution_time or 0) }}ms</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{{
|
||||||
activity.invocation_time.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
activity.invocation_time.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ show_edit_form=True,
|
|||||||
show_logs=True,
|
show_logs=True,
|
||||||
show_client=True,
|
show_client=True,
|
||||||
show_history=True,
|
show_history=True,
|
||||||
|
show_tests=True,
|
||||||
edit_url=url_for('http.editor', function_id=function_id),
|
edit_url=url_for('http.editor', function_id=function_id),
|
||||||
cancel_url=url_for('http.overview'),
|
cancel_url=url_for('http.overview'),
|
||||||
logs_url=url_for('http.logs', function_id=function_id),
|
logs_url=url_for('http.logs', function_id=function_id),
|
||||||
history_url=url_for('http.history', function_id=function_id)) }}
|
history_url=url_for('http.history', function_id=function_id),
|
||||||
|
tests_url=url_for('http.tests', function_id=function_id)) }}
|
||||||
|
|
||||||
<div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}">
|
<div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ show_edit_form=True,
|
|||||||
show_logs=True,
|
show_logs=True,
|
||||||
show_client=True,
|
show_client=True,
|
||||||
show_history=True,
|
show_history=True,
|
||||||
|
show_tests=True,
|
||||||
edit_url=edit_url,
|
edit_url=edit_url,
|
||||||
cancel_url=cancel_url,
|
cancel_url=cancel_url,
|
||||||
logs_url=url_for('http.logs', function_id=function_id),
|
logs_url=url_for('http.logs', function_id=function_id),
|
||||||
history_url=url_for('http.history', function_id=function_id)) }}
|
history_url=url_for('http.history', function_id=function_id),
|
||||||
|
tests_url=url_for('http.tests', function_id=function_id)) }}
|
||||||
|
|
||||||
|
|
||||||
<div id="app" class="">
|
<div id="app" class="">
|
||||||
|
|||||||
@@ -1,99 +1,113 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
|
|
||||||
<!-- Tabs and Actions -->
|
<!-- Tabs and Actions -->
|
||||||
<div class="border-b border-gray-200 dark:border-gray-800 flex justify-between items-end">
|
<div class="border-b border-gray-200 dark:border-gray-800 flex justify-between items-end">
|
||||||
<nav class="-mb-px flex space-x-8 overflow-x-auto" aria-label="Tabs">
|
<nav class="-mb-px flex space-x-8 overflow-x-auto" aria-label="Tabs">
|
||||||
{% if show_edit_form|default(false, true) %}
|
{% if show_edit_form|default(false, true) %}
|
||||||
<a href="#" hx-get="{{ edit_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
<a href="#" hx-get="{{ edit_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
{% if active_tab == 'edit' %}
|
{% if active_tab == 'edit' %}
|
||||||
border-blue-500 text-blue-600 dark:text-blue-400
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
{% else %}
|
{% else %}
|
||||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
{% endif %}">
|
{% endif %}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
stroke="currentColor" stroke-width="2">
|
stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||||||
</svg>
|
</svg>
|
||||||
Editor
|
Editor
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_logs|default(false, true) %}
|
{% if show_logs|default(false, true) %}
|
||||||
<a href="#" hx-get="{{ logs_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
<a href="#" hx-get="{{ logs_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
{% if active_tab == 'logs' %}
|
{% if active_tab == 'logs' %}
|
||||||
border-blue-500 text-blue-600 dark:text-blue-400
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
{% else %}
|
{% else %}
|
||||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
{% endif %}">
|
{% endif %}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
stroke="currentColor" stroke-width="2">
|
stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||||
</svg>
|
</svg>
|
||||||
Logs
|
Logs
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_client|default(false, true) %}
|
{% if show_client|default(false, true) %}
|
||||||
<a href="#" hx-get="{{ url_for('http.client', function_id=function_id) }}" hx-target="#container"
|
<a href="#" hx-get="{{ url_for('http.client', function_id=function_id) }}" hx-target="#container"
|
||||||
hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
{% if active_tab == 'client' %}
|
{% if active_tab == 'client' %}
|
||||||
border-blue-500 text-blue-600 dark:text-blue-400
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
{% else %}
|
{% else %}
|
||||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
{% endif %}">
|
{% endif %}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
stroke="currentColor" stroke-width="2">
|
stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
|
d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
|
||||||
</svg>
|
</svg>
|
||||||
Client
|
Client
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_history|default(false, true) %}
|
{% if show_history|default(false, true) %}
|
||||||
<a href="#" hx-get="{{ history_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
<a href="#" hx-get="{{ history_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
{% if active_tab == 'history' %}
|
{% if active_tab == 'history' %}
|
||||||
border-blue-500 text-blue-600 dark:text-blue-400
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
{% else %}
|
{% else %}
|
||||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
{% endif %}">
|
{% endif %}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
stroke="currentColor" stroke-width="2">
|
stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0 1 18 0Z" />
|
||||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0 1 18 0Z" />
|
</svg>
|
||||||
</svg>
|
History
|
||||||
History
|
</a>
|
||||||
</a>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if show_new|default(false, true) %}
|
{% if show_tests|default(false, true) %}
|
||||||
<a href="#" hx-get="{{ new_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
<a href="#" hx-get="{{ tests_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
|
{% if active_tab == 'tests' %}
|
||||||
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
|
{% else %}
|
||||||
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
|
{% endif %}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0 1 18 0z" />
|
||||||
|
</svg>
|
||||||
|
Tests
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_new|default(false, true) %}
|
||||||
|
<a href="#" hx-get="{{ new_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true" class="whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2
|
||||||
{% if active_tab == 'new' %}
|
{% if active_tab == 'new' %}
|
||||||
border-blue-500 text-blue-600 dark:text-blue-400
|
border-blue-500 text-blue-600 dark:text-blue-400
|
||||||
{% else %}
|
{% else %}
|
||||||
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300
|
||||||
{% endif %}">
|
{% endif %}">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
stroke="currentColor" stroke-width="2">
|
stroke-width="2">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
</svg>
|
</svg>
|
||||||
New Function
|
New Function
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="pb-2">
|
<div class="pb-2">
|
||||||
<button hx-get="{{ cancel_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
<button hx-get="{{ cancel_url }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
class="inline-flex items-center justify-center px-3 py-1.5 border border-gray-300 dark:border-gray-700 shadow-sm text-xs font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
class="inline-flex items-center justify-center px-3 py-1.5 border border-gray-300 dark:border-gray-700 shadow-sm text-xs font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1.5 text-gray-500 dark:text-gray-400"
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 mr-1.5 text-gray-500 dark:text-gray-400" fill="none"
|
||||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
</svg>
|
||||||
</svg>
|
Back
|
||||||
Back
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -8,10 +8,12 @@ show_edit_form=True,
|
|||||||
show_logs=True,
|
show_logs=True,
|
||||||
show_client=True,
|
show_client=True,
|
||||||
show_history=True,
|
show_history=True,
|
||||||
|
show_tests=True,
|
||||||
edit_url=url_for('http.editor', function_id=function_id),
|
edit_url=url_for('http.editor', function_id=function_id),
|
||||||
cancel_url=url_for('http.overview'),
|
cancel_url=url_for('http.overview'),
|
||||||
logs_url=url_for('http.logs', function_id=function_id),
|
logs_url=url_for('http.logs', function_id=function_id),
|
||||||
history_url=url_for('http.history', function_id=function_id)) }}
|
history_url=url_for('http.history', function_id=function_id),
|
||||||
|
tests_url=url_for('http.tests', function_id=function_id)) }}
|
||||||
|
|
||||||
<div id="history-view"></div>
|
<div id="history-view"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,18 @@
|
|||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
|
|
||||||
{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id,
|
|
||||||
active_tab='logs',
|
|
||||||
show_edit_form=True,
|
|
||||||
show_logs=True,
|
|
||||||
show_client=True,
|
|
||||||
{% extends 'dashboard.html' %}
|
|
||||||
|
|
||||||
{% block page %}
|
|
||||||
|
|
||||||
{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id,
|
{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id,
|
||||||
active_tab='logs',
|
active_tab='logs',
|
||||||
show_edit_form=True,
|
show_edit_form=True,
|
||||||
show_logs=True,
|
show_logs=True,
|
||||||
show_client=True,
|
show_client=True,
|
||||||
show_history=True,
|
show_history=True,
|
||||||
|
show_tests=True,
|
||||||
edit_url=url_for('http.editor', function_id=function_id),
|
edit_url=url_for('http.editor', function_id=function_id),
|
||||||
cancel_url=url_for('http.overview'),
|
cancel_url=url_for('http.overview'),
|
||||||
logs_url=url_for('http.logs', function_id=function_id),
|
logs_url=url_for('http.logs', function_id=function_id),
|
||||||
history_url=url_for('http.history', function_id=function_id)) }}
|
history_url=url_for('http.history', function_id=function_id),
|
||||||
|
tests_url=url_for('http.tests', function_id=function_id)) }}
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{{ render_partial('dashboard/analytics.html', invocations=http_function_invocations) }}
|
{{ render_partial('dashboard/analytics.html', invocations=http_function_invocations) }}
|
||||||
@@ -53,6 +46,8 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
|||||||
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
||||||
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ "%.2f"|format(invocation.execution_time or
|
||||||
|
0) }}ms</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
||||||
v{{ invocation.version_number }}
|
v{{ invocation.version_number }}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ show_refresh=False,
|
|||||||
show_logs=False,
|
show_logs=False,
|
||||||
show_client=False,
|
show_client=False,
|
||||||
show_link=False,
|
show_link=False,
|
||||||
|
show_tests=False,
|
||||||
dashboardUrl=url_for('http.overview'),
|
dashboardUrl=url_for('http.overview'),
|
||||||
cancel_url=url_for('http.overview'),
|
cancel_url=url_for('http.overview'),
|
||||||
title='New HTTP Function')
|
title='New HTTP Function')
|
||||||
|
|||||||
30
templates/dashboard/http_functions/tests.html
Normal file
30
templates/dashboard/http_functions/tests.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends 'dashboard.html' %}
|
||||||
|
|
||||||
|
{% block page %}
|
||||||
|
|
||||||
|
{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id,
|
||||||
|
active_tab='tests',
|
||||||
|
show_edit_form=True,
|
||||||
|
show_logs=True,
|
||||||
|
show_client=True,
|
||||||
|
show_history=True,
|
||||||
|
show_tests=True,
|
||||||
|
cancel_url=cancel_url,
|
||||||
|
edit_url=url_for('http.editor', function_id=function_id),
|
||||||
|
logs_url=url_for('http.logs', function_id=function_id),
|
||||||
|
history_url=url_for('http.history', function_id=function_id),
|
||||||
|
tests_url=url_for('http.tests', function_id=function_id)) }}
|
||||||
|
|
||||||
|
<div id="test-runner"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
m.mount(document.getElementById('test-runner'), {
|
||||||
|
view: function () {
|
||||||
|
return m(TestRunner, {
|
||||||
|
function_id: {{ function_id }},
|
||||||
|
function_name: '{{ name }}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block page %}
|
{% block page %}
|
||||||
<div class="p-6 max-w-7xl mx-auto">
|
<div class="p-6 max-w-7xl mx-auto">
|
||||||
<!-- Settings Navigation -->
|
<!-- Settings Navigation -->
|
||||||
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<nav class="-mb-px flex space-x-8">
|
<nav class="-mb-px flex space-x-8">
|
||||||
<a hx-get="{{ url_for('settings.api_keys') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
<a hx-get="{{ url_for('settings.api_keys') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
@@ -23,8 +23,7 @@
|
|||||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
Login History
|
Login History
|
||||||
</a>
|
</a>
|
||||||
<a hx-get="{{ url_for('settings.account') }}" hx-target="#container" hx-swap="innerHTML"
|
<a hx-get="{{ url_for('settings.account') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true"
|
||||||
hx-push-url="true"
|
|
||||||
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
class="border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 py-4 px-1 text-sm font-medium cursor-pointer">
|
||||||
Account
|
Account
|
||||||
</a>
|
</a>
|
||||||
@@ -41,8 +40,19 @@
|
|||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Entity Relationship Diagram</h2>
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Entity Relationship Diagram</h2>
|
||||||
<div id="mermaid-diagram"
|
<div id="mermaid-diagram"
|
||||||
class="bg-white dark:bg-gray-900 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
class="bg-white dark:bg-gray-900 p-4 rounded border border-gray-200 dark:border-gray-700 overflow-x-auto ">
|
||||||
<pre class="mermaid">
|
<!-- Loading Spinner -->
|
||||||
|
<div id="mermaid-loading" class="flex flex-col items-center">
|
||||||
|
<svg class="animate-spin h-12 w-12 text-blue-500 mb-3" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">Loading diagram...</p>
|
||||||
|
</div>
|
||||||
|
<pre class="mermaid invisible">
|
||||||
erDiagram
|
erDiagram
|
||||||
{% for table in schema_info %}
|
{% for table in schema_info %}
|
||||||
{{ table.table_name|upper }} {
|
{{ table.table_name|upper }} {
|
||||||
@@ -160,14 +170,20 @@ erDiagram
|
|||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.0/ace.js"></script>
|
||||||
<script>
|
<script>
|
||||||
const sqlEditor = ace.edit("sql-editor");
|
// Clean up existing editor if it exists (for HTMX reloads)
|
||||||
sqlEditor.setTheme("ace/theme/monokai");
|
if (window.sqlEditor) {
|
||||||
sqlEditor.session.setMode("ace/mode/sql");
|
window.sqlEditor.destroy();
|
||||||
sqlEditor.setOptions({ fontSize: "14px", showPrintMargin: false, showGutter: true, highlightActiveLine: true, enableLiveAutocompletion: true });
|
window.sqlEditor.container.remove();
|
||||||
sqlEditor.setValue(`SELECT * FROM http_functions`, -1);
|
}
|
||||||
|
|
||||||
|
window.sqlEditor = ace.edit("sql-editor");
|
||||||
|
window.sqlEditor.setTheme("ace/theme/monokai");
|
||||||
|
window.sqlEditor.session.setMode("ace/mode/sql");
|
||||||
|
window.sqlEditor.setOptions({ fontSize: "14px", showPrintMargin: false, showGutter: true, highlightActiveLine: true, enableLiveAutocompletion: true });
|
||||||
|
window.sqlEditor.setValue(`SELECT * FROM http_functions`, -1);
|
||||||
|
|
||||||
document.getElementById('run-query-btn').addEventListener('click', async () => {
|
document.getElementById('run-query-btn').addEventListener('click', async () => {
|
||||||
const query = sqlEditor.getValue().trim();
|
const query = window.sqlEditor.getValue().trim();
|
||||||
const runBtn = document.getElementById('run-query-btn');
|
const runBtn = document.getElementById('run-query-btn');
|
||||||
if (!query) { showMessage('Please enter a SQL query', 'error'); return; }
|
if (!query) { showMessage('Please enter a SQL query', 'error'); return; }
|
||||||
runBtn.disabled = true;
|
runBtn.disabled = true;
|
||||||
@@ -182,7 +198,7 @@ erDiagram
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('clear-query-btn').addEventListener('click', () => {
|
document.getElementById('clear-query-btn').addEventListener('click', () => {
|
||||||
sqlEditor.setValue('', -1);
|
window.sqlEditor.setValue('', -1);
|
||||||
document.getElementById('query-message').classList.add('hidden');
|
document.getElementById('query-message').classList.add('hidden');
|
||||||
document.getElementById('query-results').classList.add('hidden');
|
document.getElementById('query-results').classList.add('hidden');
|
||||||
});
|
});
|
||||||
@@ -232,6 +248,9 @@ erDiagram
|
|||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await mermaid.default.run({ querySelector: '.mermaid' });
|
await mermaid.default.run({ querySelector: '.mermaid' });
|
||||||
|
// Hide spinner and show diagram after successful rendering
|
||||||
|
document.getElementById('mermaid-loading').classList.add('hidden');
|
||||||
|
document.querySelector('.mermaid').classList.remove('invisible');
|
||||||
console.log('Mermaid diagram rendered successfully');
|
console.log('Mermaid diagram rendered successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Mermaid error:', error);
|
console.error('Mermaid error:', error);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ cancel_url=url_for('timer.overview'),
|
|||||||
logs_url=url_for('timer.logs', function_id=function_id),
|
logs_url=url_for('timer.logs', function_id=function_id),
|
||||||
history_url=url_for('timer.history', function_id=function_id)) }}
|
history_url=url_for('timer.history', function_id=function_id)) }}
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{{ render_partial('dashboard/analytics.html', invocations=timer_function_invocations) }}
|
{{ render_partial('dashboard/analytics.html', invocations=timer_function_invocations) }}
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
@@ -34,6 +34,8 @@ history_url=url_for('timer.history', function_id=function_id)) }}
|
|||||||
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
||||||
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
}}</span>
|
}}</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ "%.2f"|format(invocation.execution_time or
|
||||||
|
0) }}ms</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 rounded-full">
|
||||||
v{{ invocation.version_number }}
|
v{{ invocation.version_number }}
|
||||||
|
|||||||
@@ -2,55 +2,86 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="w-full h-screen flex items-center justify-center" data-id="1">
|
<div
|
||||||
<div class="rounded-lg border bg-card text-card-foreground shadow-sm w-full max-w-md mx-4 bg-gray-100" data-id="2"
|
class="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-950 px-4 py-12">
|
||||||
data-v0-t="card">
|
<div class="w-full max-w-md">
|
||||||
<div class="flex flex-col space-y-1.5 p-6" data-id="3">
|
<!-- Logo/Branding Section -->
|
||||||
<h1 class="text-3xl font-bold text-center" data-id="4">
|
<div class="text-center mb-8">
|
||||||
Login
|
<div
|
||||||
|
class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl shadow-lg mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Welcome Back
|
||||||
</h1>
|
</h1>
|
||||||
{% if error %}
|
<p class="text-gray-600 dark:text-gray-400">Sign in to your Function account</p>
|
||||||
<h2 class="text-2xl font-bold text-center text-red-500" data-id="4">
|
|
||||||
{{ error }}
|
|
||||||
</h2>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" class="p-4 space-y-4" data-id="5">
|
|
||||||
<div class="space-y-2" data-id="6">
|
<!-- Login Card -->
|
||||||
<label
|
<div
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 {% if error %}text-red-500 {% endif %}"
|
class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
for="username" data-id="7">
|
{% if error %}
|
||||||
Username
|
<div class="bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 px-6 py-4">
|
||||||
</label>
|
<div class="flex items-center text-red-700 dark:text-red-300">
|
||||||
<input
|
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
class="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full {% if error %}border-red-500 {% endif %}"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
name="username" placeholder="Enter your username" required="" data-id="8" type="text">
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium">{{ error }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2" data-id="9">
|
{% endif %}
|
||||||
<label
|
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 {% if error %}text-red-500 {% endif %}"
|
<form method="POST" class="p-8 space-y-6">
|
||||||
for="password" data-id="10">Password</label>
|
<!-- Username Field -->
|
||||||
<input
|
<div class="space-y-2">
|
||||||
class="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full {% if error %}border-red-500 {% endif %}"
|
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
name="password" placeholder="Enter your password" required="" data-id="11" type="password">
|
Username
|
||||||
</div>
|
</label>
|
||||||
<div class="flex justify-between items-center" data-id="12">
|
<input id="username" name="username" type="text" required autocomplete="username"
|
||||||
<a class="text-sm underline text-gray-500" data-id="13" href="#" rel="ugc">
|
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 {% if error %}border-red-500 dark:border-red-500 focus:ring-red-500{% endif %}"
|
||||||
Forgot password?
|
placeholder="Enter your username">
|
||||||
</a>
|
</div>
|
||||||
<button
|
|
||||||
class="inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-primary/90 h-10 px-6 py-2 bg-blue-500 text-white rounded-md"
|
<!-- Password Field -->
|
||||||
type="submit" data-id="14">
|
<div class="space-y-2">
|
||||||
Login
|
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input id="password" name="password" type="password" required autocomplete="current-password"
|
||||||
|
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 {% if error %}border-red-500 dark:border-red-500 focus:ring-red-500{% endif %}"
|
||||||
|
placeholder="Enter your password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forgot Password Link -->
|
||||||
|
<div class="flex items-center justify-end">
|
||||||
|
<a href="#"
|
||||||
|
class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 transition-colors">
|
||||||
|
Forgot password?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold py-3 px-6 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||||
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</form>
|
||||||
<hr class="my-4" data-id="15">
|
</div>
|
||||||
<div class="text-center" data-id="16">
|
|
||||||
<p class="text-gray-500" data-id="17">Don't have an account? <a class="underline text-blue-500"
|
<!-- Sign Up Link -->
|
||||||
data-id="18" href="{{ url_for('auth.signup') }}" rel="ugc">Sign up</a>
|
<div class="mt-6 text-center">
|
||||||
</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
</div>
|
Don't have an account?
|
||||||
</form>
|
<a href="{{ url_for('auth.signup') }}"
|
||||||
|
class="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 transition-colors">
|
||||||
|
Sign up for free
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,52 +2,83 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="w-full h-screen flex items-center justify-center" data-id="1">
|
<div
|
||||||
<div class="rounded-lg border bg-card text-card-foreground shadow-sm w-full max-w-md mx-4 bg-gray-100" data-id="2"
|
class="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-950 px-4 py-12">
|
||||||
data-v0-t="card">
|
<div class="w-full max-w-md">
|
||||||
<div class="flex flex-col space-y-1.5 p-6" data-id="3">
|
<!-- Logo/Branding Section -->
|
||||||
<h1 class="text-3xl font-bold text-center" data-id="4">
|
<div class="text-center mb-8">
|
||||||
Sign up
|
<div
|
||||||
|
class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-blue-500 to-blue-700 rounded-2xl shadow-lg mb-4">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Create Account
|
||||||
</h1>
|
</h1>
|
||||||
{% if error %}
|
<p class="text-gray-600 dark:text-gray-400">Get started with Function today</p>
|
||||||
<h2 class="text-2xl font-bold text-center text-red-500" data-id="4">
|
|
||||||
{{ error }}
|
|
||||||
</h2>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" class="p-4 space-y-4" data-id="5">
|
|
||||||
<div class="space-y-2" data-id="6">
|
<!-- Signup Card -->
|
||||||
<label
|
<div
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 {% if error %}text-red-500 {% endif %}"
|
class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||||
for="username" data-id="7">
|
{% if error %}
|
||||||
Username
|
<div class="bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 px-6 py-4">
|
||||||
</label>
|
<div class="flex items-center text-red-700 dark:text-red-300">
|
||||||
<input
|
<svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
class="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full {% if error %}border-red-500 {% endif %}"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
name="username" placeholder="Enter your username" required="" data-id="8" type="text">
|
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm font-medium">{{ error }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2" data-id="9">
|
{% endif %}
|
||||||
<label
|
|
||||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 {% if error %}text-red-500 {% endif %}"
|
<form method="POST" class="p-8 space-y-6">
|
||||||
for="password" data-id="10">Password</label>
|
<!-- Username Field -->
|
||||||
<input
|
<div class="space-y-2">
|
||||||
class="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 w-full {% if error %}border-red-500 {% endif %}"
|
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
name="password" placeholder="Enter your password" required="" data-id="11" type="password">
|
Username
|
||||||
</div>
|
</label>
|
||||||
<div class="flex justify-between items-center" data-id="12">
|
<input id="username" name="username" type="text" required autocomplete="username"
|
||||||
<button
|
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 {% if error %}border-red-500 dark:border-red-500 focus:ring-red-500{% endif %}"
|
||||||
class="inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-primary/90 h-10 px-6 py-2 bg-blue-500 text-white rounded-md"
|
placeholder="Choose a username">
|
||||||
type="submit" data-id="14">
|
</div>
|
||||||
Sign up
|
|
||||||
|
<!-- Password Field -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input id="password" name="password" type="password" required autocomplete="new-password"
|
||||||
|
class="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 {% if error %}border-red-500 dark:border-red-500 focus:ring-red-500{% endif %}"
|
||||||
|
placeholder="Create a strong password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit Button -->
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white font-semibold py-3 px-6 rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||||
|
Create Account
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
<hr class="my-4" data-id="15">
|
<!-- Terms & Privacy (Optional) -->
|
||||||
<div class="text-center" data-id="16">
|
<p class="text-xs text-center text-gray-500 dark:text-gray-400">
|
||||||
<p class="text-gray-500" data-id="17">Already have an accont? <a class="underline text-blue-500"
|
By creating an account, you agree to our Terms of Service and Privacy Policy
|
||||||
data-id="18" href="{{ url_for('auth.login') }}" rel="ugc">Login</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
|
<!-- Login Link -->
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Already have an account?
|
||||||
|
<a href="{{ url_for('auth.login') }}"
|
||||||
|
class="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 transition-colors">
|
||||||
|
Sign in
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables from .env file first, before any other imports
|
# Load environment variables from .env file first, before any other imports
|
||||||
@@ -31,6 +32,7 @@ async def execute_timer_function_async(timer_function):
|
|||||||
Execute a timer function asynchronously and record the invocation
|
Execute a timer function asynchronously and record the invocation
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
start_time = time.time()
|
||||||
code = timer_function['code']
|
code = timer_function['code']
|
||||||
environment = timer_function['environment']
|
environment = timer_function['environment']
|
||||||
name = timer_function['name']
|
name = timer_function['name']
|
||||||
@@ -111,6 +113,9 @@ async def execute_timer_function_async(timer_function):
|
|||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", [json.dumps(function_specific_env), next_run, timer_function['id']], commit=True)
|
""", [json.dumps(function_specific_env), next_run, timer_function['id']], commit=True)
|
||||||
|
|
||||||
|
# Calculate execution time in milliseconds
|
||||||
|
execution_time = (time.time() - start_time) * 1000
|
||||||
|
|
||||||
# Record the invocation
|
# Record the invocation
|
||||||
db.execute("""
|
db.execute("""
|
||||||
INSERT INTO timer_function_invocations
|
INSERT INTO timer_function_invocations
|
||||||
@@ -121,7 +126,7 @@ async def execute_timer_function_async(timer_function):
|
|||||||
response_data['status'],
|
response_data['status'],
|
||||||
json.dumps(response_data['logs']),
|
json.dumps(response_data['logs']),
|
||||||
version_number,
|
version_number,
|
||||||
response_data.get('execution_time')
|
execution_time
|
||||||
], commit=True)
|
], commit=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user