WIP: Add light/dark theme with toggle in navbar (dark theme styling incomplete - dont care for now)
This commit is contained in:
10
db.py
10
db.py
@@ -166,18 +166,22 @@ ORDER BY invocation_time DESC""", [http_function_id])
|
||||
|
||||
def get_user(self, user_id):
|
||||
user = self.execute(
|
||||
'SELECT id, username, password_hash, created_at FROM users WHERE id=%s', [int(user_id)], one=True)
|
||||
'SELECT id, username, password_hash, created_at, theme_preference FROM users WHERE id=%s', [int(user_id)], one=True)
|
||||
return user
|
||||
|
||||
def get_user_by_username(self, username):
|
||||
user = self.execute(
|
||||
'SELECT id, username, password_hash, created_at FROM users WHERE username=%s', [username], one=True)
|
||||
'SELECT id, username, password_hash, created_at, theme_preference FROM users WHERE username=%s', [username], one=True)
|
||||
return user
|
||||
|
||||
def create_new_user(self, username, password_hash):
|
||||
new_user = self.execute(
|
||||
'INSERT INTO users (username, password_hash) VALUES (%s, %s) RETURNING id, username, password_hash, created_at', [username, password_hash], commit=True, one=True)
|
||||
'INSERT INTO users (username, password_hash, theme_preference) VALUES (%s, %s, %s) RETURNING id, username, password_hash, created_at, theme_preference', [username, password_hash, 'light'], commit=True, one=True)
|
||||
return new_user
|
||||
|
||||
def update_user_theme_preference(self, user_id, theme):
|
||||
self.execute(
|
||||
'UPDATE users SET theme_preference=%s WHERE id=%s', [theme, user_id], commit=True)
|
||||
|
||||
def get_http_function_history(self, function_id):
|
||||
http_function_history = self.execute(
|
||||
|
||||
@@ -7,25 +7,26 @@ from jinja2_fragments import render_block
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
class User(UserMixin):
|
||||
def __init__(self, id, username, password_hash, created_at):
|
||||
def __init__(self, id, username, password_hash, created_at, theme_preference='light'):
|
||||
self.id = id
|
||||
self.username = username
|
||||
self.password_hash = password_hash
|
||||
self.created_at = created_at
|
||||
self.theme_preference = theme_preference
|
||||
|
||||
@staticmethod
|
||||
def get(user_id):
|
||||
user_data = db.get_user(int(user_id))
|
||||
|
||||
if user_data:
|
||||
return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'])
|
||||
return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
||||
return None
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
user_data = db.get_user(int(user_id))
|
||||
if user_data:
|
||||
return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'])
|
||||
return User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
||||
return None
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
@@ -45,7 +46,7 @@ def login():
|
||||
if not check_password_hash(user_data['password_hash'], password):
|
||||
return render_template("login.html", error="Invalid username or password")
|
||||
|
||||
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'])
|
||||
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
||||
|
||||
login_user(user)
|
||||
|
||||
@@ -72,7 +73,7 @@ def signup():
|
||||
hashed_password = generate_password_hash(password)
|
||||
user_data = db.create_new_user(username, hashed_password)
|
||||
|
||||
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'])
|
||||
user = User(id=str(user_data['id']), username=user_data['username'], password_hash=user_data['password_hash'], created_at=user_data['created_at'], theme_preference=user_data.get('theme_preference', 'light'))
|
||||
login_user(user)
|
||||
|
||||
return redirect(url_for('home.index'))
|
||||
|
||||
@@ -62,3 +62,15 @@ def delete_api_key(key_id):
|
||||
user_id = current_user.id
|
||||
db.delete_api_key(user_id, key_id)
|
||||
return "", 200
|
||||
|
||||
@settings.route("/theme", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_theme():
|
||||
user_id = current_user.id
|
||||
theme = request.form.get("theme")
|
||||
if theme in ['light', 'dark']:
|
||||
db.update_user_theme_preference(user_id, theme)
|
||||
# Return empty string as we'll handle the UI update via client-side JS or just let the class toggle persist
|
||||
# Actually, for HTMX we might want to return something or just 200 OK.
|
||||
return "", 200
|
||||
return "Invalid theme", 400
|
||||
|
||||
14
static/js/chart.js
Normal file
14
static/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
@@ -7,19 +7,30 @@ const FunctionHistory = {
|
||||
vnode.state.rightVersion = versions[0];
|
||||
vnode.state.editor = null;
|
||||
vnode.state.aceDiffer = null;
|
||||
|
||||
// Listen for theme changes
|
||||
vnode.state.themeListener = (e) => {
|
||||
const newTheme = e.detail.theme;
|
||||
const aceTheme = newTheme === 'dark' ? "ace/theme/monokai" : "ace/theme/chrome";
|
||||
if (vnode.state.editor) {
|
||||
vnode.state.editor.setTheme(aceTheme);
|
||||
}
|
||||
// Note: AceDiff might need a full re-init to change theme properly, or we can just let it be
|
||||
};
|
||||
window.addEventListener('themeChanged', vnode.state.themeListener);
|
||||
},
|
||||
|
||||
view: function (vnode) {
|
||||
const { versions } = vnode.attrs;
|
||||
const { mode, selectedVersion, leftVersion, rightVersion } = vnode.state;
|
||||
|
||||
return m(".flex.flex-col.md:flex-row.h-full", [
|
||||
return m(".flex.flex-col.md:flex-row.h-full.bg-white.dark:bg-gray-900.text-gray-900.dark:text-white", [
|
||||
// Vertical Timeline
|
||||
m(".w-full.md:w-64.border-b.md:border-r.md:border-b-0.overflow-y-auto", [
|
||||
m("div.p-4.border-b.flex.justify-between.items-center", [
|
||||
m(".w-full.md:w-64.border-b.md:border-r.md:border-b-0.border-gray-200.dark:border-gray-700.overflow-y-auto.bg-gray-50.dark:bg-gray-800", [
|
||||
m("div.p-4.border-b.border-gray-200.dark:border-gray-700.flex.justify-between.items-center", [
|
||||
m("h3.text-lg.font-semibold", "History"),
|
||||
m(
|
||||
"button.text-sm.bg-gray-200.hover:bg-gray-300.text-gray-800.font-semibold.py-1.px-2.rounded",
|
||||
"button.text-sm.bg-gray-200.hover:bg-gray-300.dark:bg-gray-700.dark:hover:bg-gray-600.text-gray-800.dark:text-gray-200.font-semibold.py-1.px-2.rounded",
|
||||
{
|
||||
onclick: () => {
|
||||
vnode.state.mode = mode === "view" ? "diff" : "view";
|
||||
@@ -35,11 +46,11 @@ const FunctionHistory = {
|
||||
selectedVersion &&
|
||||
version.version_number === selectedVersion.version_number;
|
||||
return m(
|
||||
"li.p-2.cursor-pointer",
|
||||
"li.p-2.cursor-pointer.mb-1",
|
||||
{
|
||||
class: isActive
|
||||
? "bg-gray-200 rounded"
|
||||
: "hover:bg-gray-100 rounded",
|
||||
? "bg-blue-100 dark:bg-blue-900/50 rounded"
|
||||
: "hover:bg-gray-200 dark:hover:bg-gray-700 rounded",
|
||||
onclick: () => {
|
||||
vnode.state.selectedVersion = version;
|
||||
if (mode === "diff") {
|
||||
@@ -50,7 +61,7 @@ const FunctionHistory = {
|
||||
[
|
||||
m("div.font-bold", `Version ${version.version_number}`),
|
||||
m(
|
||||
"div.text-sm.text-gray-600",
|
||||
"div.text-sm.text-gray-600.dark:text-gray-400",
|
||||
new Date(version.versioned_at).toLocaleString()
|
||||
),
|
||||
]
|
||||
@@ -60,7 +71,7 @@ const FunctionHistory = {
|
||||
]),
|
||||
|
||||
// Code Viewer or Differ
|
||||
m(".flex-1.p-4", [
|
||||
m(".flex-1.p-4.bg-white.dark:bg-gray-900", [
|
||||
mode === "view"
|
||||
? m("div", [
|
||||
m(
|
||||
@@ -72,9 +83,9 @@ const FunctionHistory = {
|
||||
: m("div", [
|
||||
m(".flex.flex-col.md:flex-row.gap-4.mb-4", [
|
||||
m(".flex-1", [
|
||||
m("label.block.text-sm.font-medium.text-gray-700", "Left"),
|
||||
m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300", "Left"),
|
||||
m(
|
||||
"select.mt-1.block.w-full.rounded-md.border-gray-300.shadow-sm",
|
||||
"select.mt-1.block.w-full.rounded-md.border-gray-300.dark:border-gray-600.shadow-sm.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white",
|
||||
{
|
||||
onchange: (e) =>
|
||||
(vnode.state.leftVersion = versions.find(
|
||||
@@ -92,9 +103,9 @@ const FunctionHistory = {
|
||||
),
|
||||
]),
|
||||
m(".flex-1", [
|
||||
m("label.block.text-sm.font-medium.text-gray-700", "Right"),
|
||||
m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300", "Right"),
|
||||
m(
|
||||
"select.mt-1.block.w-full.rounded-md.border-gray-300.shadow-sm",
|
||||
"select.mt-1.block.w-full.rounded-md.border-gray-300.dark:border-gray-600.shadow-sm.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white",
|
||||
{
|
||||
onchange: (e) =>
|
||||
(vnode.state.rightVersion = versions.find(
|
||||
@@ -128,6 +139,8 @@ const FunctionHistory = {
|
||||
|
||||
updateEditorOrDiffer: function (vnode) {
|
||||
const { mode, selectedVersion, leftVersion, rightVersion } = vnode.state;
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const theme = isDark ? "ace/theme/monokai" : "ace/theme/chrome";
|
||||
|
||||
if (mode === "view") {
|
||||
if (vnode.state.aceDiffer) {
|
||||
@@ -136,9 +149,11 @@ const FunctionHistory = {
|
||||
}
|
||||
if (!vnode.state.editor) {
|
||||
vnode.state.editor = ace.edit("editor-history");
|
||||
vnode.state.editor.setTheme("ace/theme/monokai");
|
||||
vnode.state.editor.setTheme(theme);
|
||||
vnode.state.editor.session.setMode("ace/mode/javascript");
|
||||
vnode.state.editor.setReadOnly(true);
|
||||
} else {
|
||||
vnode.state.editor.setTheme(theme);
|
||||
}
|
||||
vnode.state.editor.setValue(selectedVersion.script, -1);
|
||||
} else {
|
||||
@@ -153,6 +168,7 @@ const FunctionHistory = {
|
||||
vnode.state.aceDiffer = new AceDiff({
|
||||
element: "#diff-container",
|
||||
mode: "ace/mode/javascript",
|
||||
theme: theme,
|
||||
left: { content: leftVersion.script, editable: false },
|
||||
right: { content: rightVersion.script, editable: false },
|
||||
});
|
||||
@@ -166,5 +182,8 @@ const FunctionHistory = {
|
||||
if (vnode.state.aceDiffer) {
|
||||
vnode.state.aceDiffer.destroy();
|
||||
}
|
||||
if (vnode.state.themeListener) {
|
||||
window.removeEventListener('themeChanged', vnode.state.themeListener);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -57,7 +57,12 @@ const Editor = {
|
||||
oncreate() {
|
||||
this.editorJS = ace.edit("js-editor");
|
||||
this.editorJS.setOptions({ maxLines: 100 });
|
||||
this.editorJS.setTheme("ace/theme/github_dark");
|
||||
|
||||
// Determine initial theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const theme = isDark ? "ace/theme/monokai" : "ace/theme/chrome";
|
||||
|
||||
this.editorJS.setTheme(theme);
|
||||
this.editorJS.session.setMode(
|
||||
this.runtime === "python" ? "ace/mode/python" : "ace/mode/javascript"
|
||||
);
|
||||
@@ -70,7 +75,7 @@ const Editor = {
|
||||
|
||||
this.editorJSON = ace.edit("json-editor");
|
||||
this.editorJSON.setOptions({ maxLines: 100 });
|
||||
this.editorJSON.setTheme("ace/theme/github_dark");
|
||||
this.editorJSON.setTheme(theme);
|
||||
this.editorJSON.session.setMode("ace/mode/json");
|
||||
this.editorJSON.setValue(this.jsonValue, -1);
|
||||
|
||||
@@ -78,6 +83,20 @@ const Editor = {
|
||||
this.jsonValue = this.editorJSON.getValue();
|
||||
m.redraw();
|
||||
});
|
||||
|
||||
// Listen for theme changes
|
||||
this.themeListener = (e) => {
|
||||
const newTheme = e.detail.theme === 'dark' ? "ace/theme/monokai" : "ace/theme/chrome";
|
||||
this.editorJS.setTheme(newTheme);
|
||||
this.editorJSON.setTheme(newTheme);
|
||||
};
|
||||
window.addEventListener('themeChanged', this.themeListener);
|
||||
},
|
||||
|
||||
onremove() {
|
||||
if (this.themeListener) {
|
||||
window.removeEventListener('themeChanged', this.themeListener);
|
||||
}
|
||||
},
|
||||
|
||||
async execute() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html class="{% if current_user.is_authenticated and current_user.theme_preference == 'dark' %}dark{% endif %}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -10,6 +10,11 @@
|
||||
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='currentColor' class='w-6 h-6'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z' /%3E%3C/svg%3E%0A" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700,900&display=swap" rel="stylesheet" />
|
||||
<script src="/static/js/tailwindcss@3.2.4.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
}
|
||||
</script>
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
<script src="/static/js/hyperscript.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.1/ace.min.js"
|
||||
@@ -21,6 +26,8 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.1/theme-monokai.min.js"
|
||||
integrity="sha512-g9yptARGYXbHR9r3kTKIAzF+vvmgEieTxuuUUcHC5tKYFpLR3DR+lsisH2KZJG2Nwaou8jjYVRdbbbBQI3Bo5w=="
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.32.1/theme-chrome.min.js" crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://unpkg.com/mithril/mithril.js"></script>
|
||||
<script src="/static/js/mithril/editor.js"></script>
|
||||
@@ -42,6 +49,16 @@
|
||||
"Noto Color Emoji";
|
||||
}
|
||||
|
||||
/* Dark mode transition */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
background-image: linear-gradient(-225deg, #cbbacc 0%, #2580b3 100%);
|
||||
}
|
||||
@@ -52,13 +69,40 @@
|
||||
background-image: linear-gradient(315deg, #f39f86 0%, #f9d976 74%);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const isDark = html.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Dispatch event for components to listen to
|
||||
window.dispatchEvent(new CustomEvent('themeChanged', { detail: { theme: newTheme } }));
|
||||
|
||||
// Send preference to backend
|
||||
const formData = new FormData();
|
||||
formData.append('theme', newTheme);
|
||||
|
||||
fetch('/settings/theme', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class="leading-relaxed tracking-wide flex flex-col">
|
||||
<body class="leading-relaxed tracking-wide flex flex-col bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-50">
|
||||
<div class="container mx-auto p-4 lg:p-1 min-h-screen h-full">
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -122,14 +122,30 @@
|
||||
</svg>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<!-- Dynamic breadcrumbs could be injected here via blocks if needed -->
|
||||
</ol>
|
||||
</nav>
|
||||
</svg>
|
||||
<span class="sr-only">Sign out</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- User Menu -->
|
||||
<div class="flex items-center gap-4">
|
||||
<button onclick="toggleTheme()"
|
||||
class="p-2 rounded-full text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
title="Toggle theme">
|
||||
<!-- Sun icon (shown in dark mode) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden dark:block" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<!-- Moon icon (shown in light mode) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="h-6 w-px bg-gray-200 dark:bg-gray-800"></div>
|
||||
<a class="inline-flex items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full w-9 h-9 text-gray-500 dark:text-gray-400"
|
||||
href="{{ url_for('auth.logout') }}" title="Sign out">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
{% extends 'dashboard.html' %}
|
||||
|
||||
{% block page %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="/static/js/chart.js"></script>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Dashboard Overview</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dashboard Overview</h1>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Functions -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Total Functions</h3>
|
||||
<div class="p-2 bg-indigo-50 rounded-lg">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Functions</h3>
|
||||
<div class="p-2 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z">
|
||||
</path>
|
||||
@@ -23,37 +24,41 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.total_timer_functions + stats.total_http_functions
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.total_timer_functions +
|
||||
stats.total_http_functions
|
||||
}}</p>
|
||||
<p class="ml-2 text-sm text-gray-500">
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
({{ stats.total_http_functions }} HTTP, {{ stats.total_timer_functions }} Timer)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Invocations -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Total Invocations</h3>
|
||||
<div class="p-2 bg-blue-50 rounded-lg">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Invocations</h3>
|
||||
<div class="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.timer_invocations + stats.http_invocations }}</p>
|
||||
<p class="ml-2 text-sm text-gray-500">All time</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ stats.timer_invocations +
|
||||
stats.http_invocations }}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-gray-400">All time</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Rate -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Overall Success Rate</h3>
|
||||
<div class="p-2 bg-green-50 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Overall Success Rate</h3>
|
||||
<div class="p-2 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
@@ -63,19 +68,20 @@
|
||||
{% set total_success = stats.timer_successful_invocations + stats.http_successful_invocations %}
|
||||
{% set success_rate = (total_success / total_invocations * 100)|round(1) if total_invocations > 0 else 0 %}
|
||||
<div class="flex items-baseline">
|
||||
<p class="text-2xl font-bold text-gray-900">{{ success_rate }}%</p>
|
||||
<p class="ml-2 text-sm text-gray-500">
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ success_rate }}%</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ total_success }}/{{ total_invocations }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avg Execution Time -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-gray-500">Avg Execution Time</h3>
|
||||
<div class="p-2 bg-yellow-50 rounded-lg">
|
||||
<svg class="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Avg Execution Time</h3>
|
||||
<div class="p-2 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
|
||||
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
@@ -83,7 +89,7 @@
|
||||
</div>
|
||||
{% set avg_time = ((stats.avg_timer_execution_time or 0) + (stats.avg_http_execution_time or 0)) / 2 %}
|
||||
<div class="flex items-baseline">
|
||||
<p class="text-2xl font-bold text-gray-900">{{ "%.2f"|format(avg_time) }}s</p>
|
||||
<p class="text-2xl font-bold text-gray-900 dark:text-white">{{ "%.2f"|format(avg_time) }}s</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,16 +97,17 @@
|
||||
<!-- Charts Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||
<!-- Invocation History Chart -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100 lg:col-span-2">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">24-Hour Activity</h3>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700 lg:col-span-2">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">24-Hour Activity</h3>
|
||||
<div class="h-64">
|
||||
<canvas id="activityChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Functions -->
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Top Functions</h3>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-100 dark:border-gray-700">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Top Functions</h3>
|
||||
<div class="space-y-4">
|
||||
{% for func in top_functions %}
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -108,16 +115,16 @@
|
||||
<div
|
||||
class="w-2 h-2 rounded-full {% if func.type == 'HTTP' %}bg-indigo-500{% else %}bg-blue-500{% endif %} mr-2">
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700">{{ func.name }}</span>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ func.name }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">{{ func.invocation_count }} calls</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">{{ func.invocation_count }} calls</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No functions found.</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No functions found.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mt-6 pt-6 border-t border-gray-100">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-2">Success Trend (7 Days)</h4>
|
||||
<div class="mt-6 pt-6 border-t border-gray-100 dark:border-gray-700">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">Success Trend (7 Days)</h4>
|
||||
<div class="h-32">
|
||||
<canvas id="trendChart"></canvas>
|
||||
</div>
|
||||
@@ -126,62 +133,74 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity Table -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Recent Activity</h3>
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Activity</h3>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ url_for('http.overview') }}"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800 font-medium">View HTTP</a>
|
||||
<span class="text-gray-300">|</span>
|
||||
class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 font-medium">View
|
||||
HTTP</a>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<a href="{{ url_for('timer.overview') }}"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 font-medium">View Timers</a>
|
||||
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium">View
|
||||
Timers</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900/50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Function</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Type
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Duration</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for activity in recent_activity %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ activity.name }}
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{
|
||||
activity.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if activity.type == 'HTTP' %}bg-indigo-100 text-indigo-800{% else %}bg-blue-100 text-blue-800{% endif %}">
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if activity.type == 'HTTP' %}bg-indigo-100 text-indigo-800 dark:bg-indigo-900/30 dark:text-indigo-300{% else %}bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300{% endif %}">
|
||||
{{ activity.type }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if activity.status == 'SUCCESS' %}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Success</span>
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">Success</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">{{
|
||||
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300">{{
|
||||
activity.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{
|
||||
<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>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{
|
||||
<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>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500">No recent activity found.
|
||||
<td colspan="5" class="px-6 py-4 text-center text-sm text-gray-500 dark:text-gray-400">No recent
|
||||
activity found.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -214,8 +233,22 @@
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: { beginAtZero: true, grid: { display: false } },
|
||||
x: { grid: { display: false } }
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
display: false,
|
||||
color: document.documentElement.classList.contains('dark') ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
ticks: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#6b7280'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: { display: false },
|
||||
ticks: {
|
||||
color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#6b7280'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -256,5 +289,21 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update charts on theme change
|
||||
window.addEventListener('themeChanged', (e) => {
|
||||
const isDark = e.detail.theme === 'dark';
|
||||
const textColor = isDark ? '#9ca3af' : '#6b7280';
|
||||
const gridColor = isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
||||
|
||||
// Update Activity Chart
|
||||
const activityChart = Chart.getChart('activityChart');
|
||||
if (activityChart) {
|
||||
activityChart.options.scales.x.ticks.color = textColor;
|
||||
activityChart.options.scales.y.ticks.color = textColor;
|
||||
activityChart.options.scales.y.grid.color = gridColor;
|
||||
activityChart.update();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,26 +3,29 @@
|
||||
{% for name, children in functions.items() %}
|
||||
{% if children['_is_function'] is not defined %}
|
||||
<details class="group" {% if path_prefix=='' %}open{% endif %}>
|
||||
<summary class="flex items-center gap-2 py-2 cursor-pointer text-gray-700 hover:text-gray-900 font-medium">
|
||||
<summary
|
||||
class="flex items-center gap-2 py-2 cursor-pointer text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white font-medium">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-5 h-5 text-gray-400 group-open:rotate-90 transition-transform">
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-gray-400 dark:text-gray-500 group-open:rotate-90 transition-transform">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
<span>{{ name }}</span>
|
||||
</summary>
|
||||
<div class="ml-4 border-l border-gray-200">
|
||||
<div class="ml-4 border-l border-gray-200 dark:border-gray-700">
|
||||
{{ render_functions(children, path_prefix + name + '/') }}
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
{% set function = children %}
|
||||
<div class="flex items-center gap-2 py-2 pl-5 text-gray-800 hover:bg-gray-50 rounded-md">
|
||||
<div
|
||||
class="flex items-center gap-2 py-2 pl-5 text-gray-800 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md">
|
||||
<div class="flex-grow flex items-center gap-2 cursor-pointer"
|
||||
hx-get="{{ url_for('http.editor', function_id=function['id']) }}" hx-target="#container" hx-swap="innerHTML"
|
||||
hx-push-url="true">
|
||||
{% if function.is_public %}
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<svg class="w-2 h-2 mr-1.5 fill-current" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
@@ -30,18 +33,21 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="font-medium">{{ function.name.split('/')[-1] }}</span>
|
||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
<span
|
||||
class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
v{{ function.version_number }}
|
||||
</span>
|
||||
<span class="bg-purple-100 text-purple-800 text-xs font-medium px-2.5 py-0.5 rounded-full uppercase">
|
||||
<span
|
||||
class="bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 text-xs font-medium px-2.5 py-0.5 rounded-full uppercase">
|
||||
{{ function.runtime }}
|
||||
</span>
|
||||
<span class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
<span
|
||||
class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
{{ function.invoked_count }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-shrink-0 flex items-center gap-2">
|
||||
<button class="p-1 text-gray-400 hover:text-gray-600"
|
||||
<button class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
hx-get="{{ url_for('http.logs', function_id=function['id']) }}" hx-target="#container"
|
||||
hx-swap="innerHTML" hx-push-url="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
||||
@@ -50,7 +56,7 @@
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-1 text-gray-400 hover:text-gray-600"
|
||||
<button class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
hx-get="{{ url_for('http.client', function_id=function['id']) }}" hx-target="#container"
|
||||
hx-swap="innerHTML" hx-push-url="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24"
|
||||
@@ -62,7 +68,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<a href="{{ url_for('execute_http_function', user_id=function.user_id, function=function.name) }}"
|
||||
target="_blank" class="p-1 text-gray-400 hover:text-gray-600">
|
||||
target="_blank" class="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
|
||||
|
||||
@@ -16,6 +16,54 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
<div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatTime(milliseconds) {
|
||||
if (milliseconds < 1000) {
|
||||
return `${milliseconds.toFixed(2)} ms`; // Display milliseconds directly if less than 1 second
|
||||
}
|
||||
|
||||
const seconds = milliseconds / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(2)} s`; // Display seconds if less than 1 minute
|
||||
}
|
||||
|
||||
const minutes = seconds / 60;
|
||||
if (minutes < 60) {
|
||||
return `${minutes.toFixed(2)} min`; // Display minutes if less than 1 hour
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
return `${hours.toFixed(2)} h`; // Display hours for longer durations
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} bytes`;
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(2)} KB`;
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
{% extends 'dashboard.html' %}
|
||||
|
||||
{% block page %}
|
||||
|
||||
{
|
||||
{
|
||||
render_partial('dashboard/http_functions/header.html', user_id = user_id, function_id = function_id,
|
||||
active_tab = 'client',
|
||||
show_edit_form = True,
|
||||
show_logs = True,
|
||||
show_client = True,
|
||||
show_history = True,
|
||||
edit_url = url_for('http.editor', function_id = function_id),
|
||||
cancel_url = url_for('http.overview'),
|
||||
logs_url = url_for('http.logs', function_id = function_id),
|
||||
history_url = url_for('http.history', function_id = function_id))
|
||||
}
|
||||
}
|
||||
|
||||
<div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatTime(milliseconds) {
|
||||
if (milliseconds < 1000) {
|
||||
@@ -51,24 +99,24 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
var KeyValueList = (initialVnode) => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return m("div.px-4.py-4.rounded-b-lg.border.border-t-0.border-gray-300", [
|
||||
return m("div.px-4.py-4.rounded-b-lg.border.border-t-0.border-gray-300.dark:border-gray-700.bg-white.dark:bg-gray-800", [
|
||||
m("div", [
|
||||
m("div.flex-col.space-y-2.mb-3", vnode.attrs.list.map(({ key, value }, index) => m('div.flex', [
|
||||
m("input.px-4.py-1.5.w-full.border.border-gray-300.rounded-md.hover:border-orange-500.focus:outline-orange-500", {
|
||||
m("input.px-4.py-1.5.w-full.border.border-gray-300.dark:border-gray-600.rounded-md.hover:border-orange-500.focus:outline-orange-500.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white", {
|
||||
placeholder: "Key",
|
||||
value: key,
|
||||
oninput: (e) => vnode.attrs.list[index].key = e.target.value
|
||||
}),
|
||||
m("input.ml-3.px-4.py-1.5.w-full.border.border-gray-300.rounded-md.hover:border-orange-500.focus:outline-orange-500", {
|
||||
m("input.ml-3.px-4.py-1.5.w-full.border.border-gray-300.dark:border-gray-600.rounded-md.hover:border-orange-500.focus:outline-orange-500.bg-white.dark:bg-gray-700.text-gray-900.dark:text-white", {
|
||||
placeholder: "Value",
|
||||
value: value,
|
||||
oninput: (e) => vnode.attrs.list[index].value = e.target.value
|
||||
}),
|
||||
m("button.ml-4.px-4.rounded-md.text-red-500.border.border-red-300.hover:bg-red-100", {
|
||||
m("button.ml-4.px-4.rounded-md.text-red-500.border.border-red-300.hover:bg-red-100.dark:border-red-800.dark:hover:bg-red-900/30", {
|
||||
onclick: () => vnode.attrs.list.splice(index, 1)
|
||||
}, "Remove")
|
||||
]))),
|
||||
m("button.px-6.py-1.rounded-md.text-orange-600.border.border-orange-400.hover:bg-orange-100", {
|
||||
m("button.px-6.py-1.rounded-md.text-orange-600.border.border-orange-400.hover:bg-orange-100.dark:text-orange-400.dark:border-orange-500.dark:hover:bg-orange-900/30", {
|
||||
onclick: () => vnode.attrs.list.push({ key: '', value: '' })
|
||||
}, "Add")
|
||||
])
|
||||
@@ -85,7 +133,7 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
oninput: (e) => this.body = e.target.value,
|
||||
value: this.body,
|
||||
rows: 4,
|
||||
class: "block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300"
|
||||
class: "block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
})
|
||||
}
|
||||
return tabMappings[this.activeTab];
|
||||
@@ -150,13 +198,13 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
view: function () {
|
||||
return m("div.mx-auto.w-full.pt-4", [
|
||||
m("form.flex", [
|
||||
m("select.px-4.py-2.border.rounded-md.border-gray-300.hover:border-orange-500.focus:outline-none.bg-gray-100", {
|
||||
m("select.px-4.py-2.border.rounded-md.border-gray-300.hover:border-orange-500.focus:outline-none.bg-gray-100.dark:bg-gray-700.dark:border-gray-600.dark:text-white", {
|
||||
onchange: (e) => this.method = e.target.value,
|
||||
value: this.method
|
||||
}, ["GET", "POST", "PUT", "PATCH", "DELETE"].map(method =>
|
||||
m("option", { value: method }, method)
|
||||
)),
|
||||
m("input.ml-3.w-full.px-4.py-2.border.rounded-md.border-gray-300.hover:border-orange-500.focus:outline-orange-500", {
|
||||
m("input.ml-3.w-full.px-4.py-2.border.rounded-md.border-gray-300.hover:border-orange-500.focus:outline-orange-500.bg-white.dark:bg-gray-700.dark:border-gray-600.dark:text-white", {
|
||||
onchange: (e) => this.url = e.target.value,
|
||||
value: this.url,
|
||||
placeholder: "URL"
|
||||
@@ -175,14 +223,14 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
// Tabs for query params, headers, and body
|
||||
m("div", [
|
||||
// Tab headers
|
||||
m("ul.flex.mt-5.border.border-gray-300.rounded-t-lg", [
|
||||
m("ul.flex.mt-5.border.border-gray-300.dark:border-gray-700.rounded-t-lg.bg-gray-50.dark:bg-gray-800", [
|
||||
[
|
||||
{ name: "Query Params", count: this.queryParams.filter(p => p.key && p.value).length },
|
||||
{ name: "Headers", count: this.headers.filter(p => p.key && p.value).length },
|
||||
{ name: "Body", count: null }
|
||||
].map(({ name, count }, index) =>
|
||||
m("li.mr-3.py-2.px-4.border-orange-400.focus:outline-none.hover:text-orange-500.cursor-pointer", {
|
||||
class: this.activeTab === name ? "border-b-2 text-orange-600" : "",
|
||||
m("li.mr-3.py-2.px-4.border-orange-400.focus:outline-none.hover:text-orange-500.cursor-pointer.text-gray-700.dark:text-gray-300", {
|
||||
class: this.activeTab === name ? "border-b-2 text-orange-600 dark:text-orange-400" : "",
|
||||
onclick: () => this.activeTab = name
|
||||
}, `${name}${count ? ` (${count})` : ``}`))
|
||||
]),
|
||||
@@ -193,17 +241,17 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
oninput: (e) => this.body = e.target.value,
|
||||
value: this.body,
|
||||
rows: 4,
|
||||
class: "block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300"
|
||||
class: "block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
}) : ''
|
||||
]),
|
||||
this.response ? m("div.my-4.p-4.shadow.rounded-lg", [
|
||||
this.response ? m("div.my-4.p-4.shadow.rounded-lg.bg-white.dark:bg-gray-800.border.border-gray-200.dark:border-gray-700", [
|
||||
m("div.flex.items-center.mb-4.justify-between", [
|
||||
m("ul.flex.border.border-gray-300.rounded-t-lg", [{ name: 'Browser Preview', count: null }, { name: 'Headers', count: this.responseHeaders.length }, { name: 'Body', count: null }].map(({ name, count }) =>
|
||||
m("li.mr-3.py-2.px-4.border-orange-400.focus:outline-none.hover:text-orange-500.cursor-pointer", {
|
||||
class: this.activeResponseTab === name ? "border-b-2 text-orange-600" : "",
|
||||
m("ul.flex.border.border-gray-300.dark:border-gray-700.rounded-t-lg.bg-gray-50.dark:bg-gray-800", [{ name: 'Browser Preview', count: null }, { name: 'Headers', count: this.responseHeaders.length }, { name: 'Body', count: null }].map(({ name, count }) =>
|
||||
m("li.mr-3.py-2.px-4.border-orange-400.focus:outline-none.hover:text-orange-500.cursor-pointer.text-gray-700.dark:text-gray-300", {
|
||||
class: this.activeResponseTab === name ? "border-b-2 text-orange-600 dark:text-orange-400" : "",
|
||||
onclick: () => this.activeResponseTab = name
|
||||
}, `${name}${count ? ` (${count})` : ``}`))),
|
||||
m("svg", { class: "h-6 w-6 cursor-pointer", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", onclick: () => this.response = '' },
|
||||
m("svg", { class: "h-6 w-6 cursor-pointer text-gray-500 dark:text-gray-400", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", onclick: () => this.response = '' },
|
||||
m("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M6 18L18 6M6 6l12 12" })
|
||||
)
|
||||
]),
|
||||
@@ -225,15 +273,15 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
)
|
||||
])
|
||||
]) : null,
|
||||
this.activeResponseTab == 'Body' ? m("div.overflow-auto.max-h-64.bg-gray-100.p-2.rounded-lg", [
|
||||
m("pre.text-sm", this.response)
|
||||
this.activeResponseTab == 'Body' ? m("div.overflow-auto.max-h-64.bg-gray-100.dark:bg-gray-900.p-2.rounded-lg", [
|
||||
m("pre.text-sm.text-gray-900.dark:text-gray-300", this.response)
|
||||
]) : null,
|
||||
this.activeResponseTab == 'Browser Preview' ? m("div.p-2.rounded-lg", m.trust(this.response)) : null,
|
||||
this.activeResponseTab == 'Browser Preview' ? m("div.p-2.rounded-lg.bg-white", m.trust(this.response)) : null,
|
||||
m("div.flex", [
|
||||
m("div.grow", ""),
|
||||
m("div.flex.space-x-2.pt-1", [{ key: 'Status', value: this.status }, { key: 'Time', value: formatTime(this.duration) }, { key: 'Size', value: formatSize(this.responseSize) }].map(({ key, value }) =>
|
||||
m("div.flex.items-center.justify-between", [
|
||||
m("span.text-gray-600", `${key}:`),
|
||||
m("span.text-gray-600.dark:text-gray-400", `${key}:`),
|
||||
m("span.font-semibold.pl-1.text-green-400", value)
|
||||
])))
|
||||
])
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
{% 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,
|
||||
active_tab='logs',
|
||||
show_edit_form=True,
|
||||
@@ -15,23 +24,25 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
|
||||
<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) }}
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<!-- Headers -->
|
||||
<div class="grid md:grid-cols-4 gap-4 bg-gray-50 p-4 border-b border-gray-200">
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Timestamp</div>
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Request</div>
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Response</div>
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Logs</div>
|
||||
<div
|
||||
class="grid md:grid-cols-4 gap-4 bg-gray-50 dark:bg-gray-700 p-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Timestamp</div>
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Request</div>
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Response</div>
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Logs</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Rows -->
|
||||
{% for invocation in http_function_invocations %}
|
||||
<div
|
||||
class="log-row grid md:grid-cols-4 gap-4 p-4 hover:bg-gray-50 transition-colors duration-150 border-b border-gray-200">
|
||||
class="log-row grid md:grid-cols-4 gap-4 p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150 border-b border-gray-200 dark:border-gray-700">
|
||||
<!-- Timestamp and Status -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div
|
||||
class="{{ 'bg-green-100 text-green-700' if invocation.status == 'SUCCESS' else 'bg-red-100 text-red-700' }} p-2 rounded-full">
|
||||
class="{{ 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' if invocation.status == 'SUCCESS' else 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' }} p-2 rounded-full">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="{{ 'M5 13l4 4L19 7' if invocation.status == 'SUCCESS' else 'M6 18L18 6M6 6l12 12' }}">
|
||||
@@ -39,28 +50,31 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-gray-900">{{ invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
||||
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}}</span>
|
||||
<span
|
||||
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 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 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request Data -->
|
||||
<div class="bg-gray-50 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 whitespace-pre-wrap">{{ invocation.request_data }}</pre>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre
|
||||
class="text-sm font-mono text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ invocation.request_data }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Response Data -->
|
||||
<div class="bg-gray-50 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 whitespace-pre-wrap">{{ invocation.response_data }}</pre>
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre
|
||||
class="text-sm font-mono text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ invocation.response_data }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="bg-gray-50 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 whitespace-pre-wrap">{% if invocation.logs and invocation.logs[0] is iterable and invocation.logs[0] is not string -%}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{% if invocation.logs and invocation.logs[0] is iterable and invocation.logs[0] is not string -%}
|
||||
{{- invocation.logs | map('join', ' ') | join('\n') -}}
|
||||
{%- elif invocation.logs is iterable and invocation.logs is not string -%}
|
||||
{{- invocation.logs | join('\n') -}}
|
||||
@@ -68,7 +82,7 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
{{- invocation.logs | string -}}
|
||||
{%- endif -%}</pre>
|
||||
{% if not invocation.logs %}
|
||||
<div class="text-sm text-gray-400 italic">No logs available</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-500 italic">No logs available</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,14 +90,14 @@ history_url=url_for('http.history', function_id=function_id)) }}
|
||||
|
||||
{% if not http_function_invocations %}
|
||||
<div class="p-8 text-center">
|
||||
<div class="text-gray-400">
|
||||
<div class="text-gray-400 dark:text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No logs found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">No function invocations have been recorded yet.</p>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No logs found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">No function invocations have been recorded yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -1,82 +1,25 @@
|
||||
{% extends 'dashboard.html' %}
|
||||
const isExpanding = btn.textContent.trim() === 'Expand All';
|
||||
|
||||
{% block page %}
|
||||
details.forEach(detail => {
|
||||
if (isExpanding) {
|
||||
detail.setAttribute('open', '');
|
||||
} else {
|
||||
detail.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
|
||||
<div class="px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex items-center mb-6" data-id="51">
|
||||
<h1 class="text-2xl font-bold text-gray-900">HTTP Functions</h1>
|
||||
<button
|
||||
class="inline-flex items-center px-4 py-2 ml-auto bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors duration-200"
|
||||
hx-get="{{ url_for('http.new') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" class="w-5 h-5 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"></path>
|
||||
</svg>
|
||||
Add Function
|
||||
</button>
|
||||
</div>
|
||||
btn.textContent = isExpanding ? 'Collapse All' : 'Expand All';
|
||||
}
|
||||
|
||||
<div class="mb-6 flex gap-4">
|
||||
<input type="text" name="q" placeholder="Search functions..."
|
||||
class="flex-grow px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||||
value="{{ search_query }}" hx-get="{{ url_for('http.overview') }}" hx-trigger="keyup changed delay:500ms"
|
||||
hx-target="#function-list" hx-headers='{"HX-Target": "function-list"}' hx-push-url="true">
|
||||
|
||||
<button onclick="toggleAllDetails()" id="toggle-btn"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 font-medium transition-colors duration-200 whitespace-nowrap">
|
||||
Expand All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="function-list">
|
||||
{% block function_list %}
|
||||
{% from 'dashboard/http_functions/_function_list.html' import render_functions %}
|
||||
{{ render_functions(http_functions) }}
|
||||
|
||||
{% if http_functions|length == 0 %}
|
||||
<div class="py-12">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
aria-hidden="true">
|
||||
<path vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-lg font-medium text-gray-900">No functions found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started by creating a new function.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleAllDetails() {
|
||||
const details = document.querySelectorAll('#function-list details');
|
||||
const btn = document.getElementById('toggle-btn');
|
||||
const isExpanding = btn.textContent.trim() === 'Expand All';
|
||||
|
||||
details.forEach(detail => {
|
||||
if (isExpanding) {
|
||||
detail.setAttribute('open', '');
|
||||
} else {
|
||||
detail.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
|
||||
btn.textContent = isExpanding ? 'Collapse All' : 'Expand All';
|
||||
}
|
||||
|
||||
// Reset button state when HTMX updates the list
|
||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
||||
if (evt.detail.target.id === 'function-list') {
|
||||
const btn = document.getElementById('toggle-btn');
|
||||
if (btn) {
|
||||
btn.textContent = 'Expand All';
|
||||
}
|
||||
}
|
||||
});
|
||||
// Reset button state when HTMX updates the list
|
||||
document.body.addEventListener('htmx:afterSwap', function (evt) {
|
||||
if (evt.detail.target.id === 'function-list') {
|
||||
const btn = document.getElementById('toggle-btn');
|
||||
if (btn) {
|
||||
btn.textContent = 'Expand All';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -14,25 +14,28 @@ 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">
|
||||
{{ render_partial('dashboard/analytics.html', invocations=timer_function_invocations) }}
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<!-- Headers -->
|
||||
<div class="grid md:grid-cols-3 gap-4 bg-gray-50 p-4 border-b border-gray-200">
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Invocation Time</div>
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Status</div>
|
||||
<div class="font-semibold text-gray-600 md:block hidden">Logs</div>
|
||||
<div
|
||||
class="grid md:grid-cols-3 gap-4 bg-gray-50 dark:bg-gray-700 p-4 border-b border-gray-200 dark:border-gray-600">
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Invocation Time</div>
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Status</div>
|
||||
<div class="font-semibold text-gray-600 dark:text-gray-300 md:block hidden">Logs</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Rows -->
|
||||
{% for invocation in timer_function_invocations %}
|
||||
<div
|
||||
class="log-row grid md:grid-cols-3 gap-4 p-4 hover:bg-gray-50 transition-colors duration-150 border-b border-gray-200">
|
||||
class="log-row grid md:grid-cols-3 gap-4 p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors duration-150 border-b border-gray-200 dark:border-gray-700">
|
||||
<!-- Timestamp -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm text-gray-900">{{ invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
<span class="text-sm text-gray-900 dark:text-gray-200">{{
|
||||
invocation.invocation_time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}}</span>
|
||||
<span
|
||||
class="inline-flex items-center w-fit px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 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 }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -41,14 +44,14 @@ history_url=url_for('timer.history', function_id=function_id)) }}
|
||||
<!-- Status -->
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="{{ 'bg-green-100 text-green-700' if invocation.status == 'SUCCESS' else 'bg-red-100 text-red-700' }} px-3 py-1 rounded-full text-sm">
|
||||
class="{{ 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300' if invocation.status == 'SUCCESS' else 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' }} px-3 py-1 rounded-full text-sm">
|
||||
{{ invocation.status }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs -->
|
||||
<div class="bg-gray-50 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 whitespace-pre-wrap">{% if invocation.logs and invocation.logs[0] is iterable and invocation.logs[0] is not string -%}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 overflow-auto max-h-40">
|
||||
<pre class="text-sm font-mono text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{% if invocation.logs and invocation.logs[0] is iterable and invocation.logs[0] is not string -%}
|
||||
{{- invocation.logs | map('join', ' ') | join('\n') -}}
|
||||
{%- elif invocation.logs is iterable and invocation.logs is not string -%}
|
||||
{{- invocation.logs | join('\n') -}}
|
||||
@@ -56,7 +59,7 @@ history_url=url_for('timer.history', function_id=function_id)) }}
|
||||
{{- invocation.logs | string -}}
|
||||
{%- endif -%}</pre>
|
||||
{% if not invocation.logs %}
|
||||
<div class="text-sm text-gray-400 italic">No logs available</div>
|
||||
<div class="text-sm text-gray-400 dark:text-gray-500 italic">No logs available</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,14 +67,15 @@ history_url=url_for('timer.history', function_id=function_id)) }}
|
||||
|
||||
{% if not timer_function_invocations %}
|
||||
<div class="p-8 text-center">
|
||||
<div class="text-gray-400">
|
||||
<div class="text-gray-400 dark:text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No logs found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">No timer function invocations have been recorded yet.</p>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No logs found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">No timer function invocations have been recorded
|
||||
yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="flex items-center justify-between mb-8" data-id="51">
|
||||
<h1 class="text-2xl font-bold text-gray-900">Timer Functions</h1>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Timer Functions</h1>
|
||||
<button
|
||||
class="inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors duration-200"
|
||||
hx-get="{{ url_for('timer.new') }}" hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
|
||||
@@ -16,32 +16,38 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-gray-200 rounded-lg shadow-sm overflow-hidden min-h-[200px]">
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm overflow-hidden min-h-[200px]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 border-b border-gray-200">
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900">Name</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900">Schedule</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900">Next Run</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900">Status</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900">Actions</th>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-gray-200">Name</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-gray-200">Schedule
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-gray-200">Next Run
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-gray-200">Status
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-900 dark:text-gray-200">Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for function in timer_functions %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2 cursor-pointer"
|
||||
hx-get="{{ url_for('timer.edit', function_id=function.id) }}" hx-target="#container"
|
||||
hx-swap="innerHTML" hx-push-url="true">
|
||||
<span class="font-medium text-gray-900">{{ function.name }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ function.name }}</span>
|
||||
<span
|
||||
class="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
{{ function.invocation_count }}
|
||||
</span>
|
||||
{% if function.last_run %}
|
||||
<span class="bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
<span
|
||||
class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300 text-xs font-medium px-2.5 py-0.5 rounded-full">
|
||||
Last run: {{ function.last_run.strftime('%Y-%m-%d %H:%M') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -49,18 +55,20 @@
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if function.trigger_type == 'interval' %}
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1.5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<span class="inline-flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||
</svg>
|
||||
Every {{ function.frequency_minutes }} minutes
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center">
|
||||
<svg class="w-4 h-4 mr-1.5 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<span class="inline-flex items-center text-gray-700 dark:text-gray-300">
|
||||
<svg class="w-4 h-4 mr-1.5 text-gray-500 dark:text-gray-400"
|
||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />
|
||||
</svg>
|
||||
@@ -70,15 +78,16 @@
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if function.next_run %}
|
||||
<span class="text-gray-900">{{ function.next_run.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
<span class="text-gray-900 dark:text-gray-300">{{ function.next_run.strftime('%Y-%m-%d
|
||||
%H:%M') }}</span>
|
||||
{% else %}
|
||||
<span class="text-gray-500">-</span>
|
||||
<span class="text-gray-500 dark:text-gray-400">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if function.enabled %}
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<svg class="w-2 h-2 mr-1.5 fill-current" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
@@ -86,7 +95,7 @@
|
||||
</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
||||
<svg class="w-2 h-2 mr-1.5 fill-current" viewBox="0 0 8 8">
|
||||
<circle cx="4" cy="4" r="3" />
|
||||
</svg>
|
||||
@@ -97,7 +106,7 @@
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="inline-flex items-center p-1.5 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100"
|
||||
class="inline-flex items-center p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="Edit" hx-get="{{ url_for('timer.edit', function_id=function.id) }}"
|
||||
hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none"
|
||||
@@ -107,7 +116,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center p-1.5 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100"
|
||||
class="inline-flex items-center p-1.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
title="{{ 'Pause' if function.enabled else 'Resume' }}"
|
||||
hx-post="{{ url_for('timer.toggle', function_id=function.id) }}"
|
||||
hx-target="#container" hx-swap="innerHTML">
|
||||
@@ -134,9 +143,10 @@
|
||||
|
||||
{% if timer_functions|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-16 text-center">
|
||||
<p class="text-gray-500 text-lg">No functions found</p>
|
||||
<p class="text-gray-400 text-sm mt-2">Click the "Add Function" button to create your first
|
||||
<td colspan="5" class="px-6 py-16 text-center">
|
||||
<p class="text-gray-500 dark:text-gray-400 text-lg">No functions found</p>
|
||||
<p class="text-gray-400 dark:text-gray-500 text-sm mt-2">Click the "Add Function" button to
|
||||
create your first
|
||||
function</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user