diff --git a/.gitignore b/.gitignore index e8ea323..3e8e158 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.env +__pycache__/ diff --git a/app.py b/app.py index 811eda7..faa7116 100644 --- a/app.py +++ b/app.py @@ -151,24 +151,6 @@ def create_http_function(): print(e) return { "status": "error", "message": str(e) } -@ app.route("/dashboard/http_functions//edit_form", methods=["GET"]) -@login_required -def get_http_function_edit_form(function_id): - 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'] - script = http_function['script_content'] - environment_info = json.dumps(http_function['environment_info'], indent=2) - is_public = http_function['is_public'] - log_request = http_function['log_request'] - log_response = http_function['log_response'] - version_number = http_function['version_number'] - - if htmx: - return render_block(app.jinja_env, 'dashboard/http_functions/edit.html', 'page', user_id=user_id, function_id=function_id, name=name, script=script, environment_info=environment_info, is_public=is_public, log_request=log_request, log_response=log_response, version_number=version_number) - return render_template("dashboard/http_functions/edit.html", user_id=user_id, name=name, function_id=function_id, script=script, environment_info=environment_info, is_public=is_public, log_request=log_request, log_response=log_response, version_number=version_number) @ app.route("/dashboard/http_functions//edit", methods=["POST"]) @login_required @@ -437,6 +419,129 @@ def logout(): logout_user() return redirect(url_for('home')) +@app.route("/http_function_editor/", methods=["GET"]) +@login_required +def http_function_editor(function_id): + 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 + + # Create a view model with all necessary data for the editor + editor_data = { + 'id': http_function['id'], + 'name': http_function['name'], + 'script_content': http_function['script_content'], + 'environment_info': json.dumps(http_function['environment_info'], indent=2), + 'is_public': http_function['is_public'], + 'log_request': http_function['log_request'], + 'log_response': http_function['log_response'], + 'version_number': http_function['version_number'], + 'user_id': user_id, + 'function_id': function_id, + # Add new URLs for navigation + 'cancel_url': url_for('dashboard_http_functions'), + 'edit_url': url_for('http_function_editor', function_id=function_id), + } + + if htmx: + return render_block(app.jinja_env, "dashboard/http_functions/editor.html", "page", **editor_data) + + return render_template("dashboard/http_functions/editor.html", **editor_data) + +@app.route("/api/http_functions", methods=["POST"]) +@login_required +def api_create_http_function(): + try: + user_id = current_user.id + data = request.get_json() + name = data.get('name') + script_content = data.get('script_content') + environment_info = data.get('environment_info') + is_public = data.get('is_public') + log_request = data.get('log_request') + log_response = data.get('log_response') + + # Check if function with same name already exists for this user + existing_function = db.get_http_function(user_id, name) + if existing_function: + return jsonify({ + "status": "error", + "message": f"A function with the name '{name}' already exists" + }), 400 + + http_function = db.create_new_http_function( + user_id, + name, + script_content, + environment_info, + is_public, + log_request, + log_response + ) + + return jsonify({ + "status": "success", + "message": f'{name} created', + "function": http_function + }) + except Exception as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 400 + +@app.route("/api/http_functions/", methods=["POST"]) +@login_required +def api_update_http_function(function_id): + try: + user_id = current_user.id + data = request.get_json() + name = data.get('name') + script_content = data.get('script_content') + environment_info = data.get('environment_info') + is_public = data.get('is_public') + log_request = data.get('log_request') + log_response = data.get('log_response') + + updated_function = db.edit_http_function( + user_id, + function_id, + name, + script_content, + environment_info, + is_public, + log_request, + log_response + ) + + return jsonify({ + "status": "success", + "message": f'{name} updated', + "function": updated_function + }) + except Exception as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 400 + +@app.route("/api/http_functions/", methods=["DELETE"]) +@login_required +def api_delete_http_function(function_id): + try: + user_id = current_user.id + db.delete_http_function(user_id, function_id) + + return jsonify({ + "status": "success", + "message": "Function deleted successfully" + }) + except Exception as e: + return jsonify({ + "status": "error", + "message": str(e) + }), 400 @login_manager.user_loader def load_user(user_id): diff --git a/static/js/mithril/alert.js b/static/js/mithril/alert.js new file mode 100644 index 0000000..e8da3a5 --- /dev/null +++ b/static/js/mithril/alert.js @@ -0,0 +1,40 @@ +const Alert = { + show(message, type = 'success') { + const alert = document.createElement('div'); + alert.className = `fixed top-4 right-4 p-4 rounded-md text-white ${ + type === 'success' ? 'bg-green-500' : 'bg-red-500' + } transition-opacity duration-500`; + + alert.innerHTML = ` +
+ + ${type === 'success' ? ` + + + + ` : ` + + + + `} + + ${message} +
+ `; + + document.body.appendChild(alert); + + // Fade in + setTimeout(() => { + alert.style.opacity = '1'; + }, 10); + + // Fade out and remove + setTimeout(() => { + alert.style.opacity = '0'; + setTimeout(() => { + document.body.removeChild(alert); + }, 500); + }, 3000); + } +}; \ No newline at end of file diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js new file mode 100644 index 0000000..09a2872 --- /dev/null +++ b/static/js/mithril/editor.js @@ -0,0 +1,496 @@ +const Editor = { + oninit(vnode) { + // Props + this.isEdit = vnode.attrs.isEdit || false; + this.isAdd = vnode.attrs.isAdd || false; + this.isPublic = vnode.attrs.isPublic || false; + this.logRequest = vnode.attrs.logRequest || false; + this.logResponse = vnode.attrs.logResponse || false; + + // Only controls whether the name/version is shown (left side of header), + // but we still always show the Execute button on the right side. + this.showHeader = vnode.attrs.showHeader !== false; // default true + + // New props for showing/hiding individual settings + this.showPublicToggle = vnode.attrs.showPublicToggle !== false; // default true + this.showLogRequestToggle = vnode.attrs.showLogRequestToggle !== false; // default true + this.showLogResponseToggle = vnode.attrs.showLogResponseToggle !== false; // default true + this.showSaveButton = vnode.attrs.showSaveButton !== false; // default true + + // New prop to control entire settings panel visibility + this.showFunctionSettings = vnode.attrs.showFunctionSettings !== false; // default true + + // Name + version + this.name = vnode.attrs.name || "foo"; + this.versionNumber = vnode.attrs.versionNumber || "1"; + this.nameEditing = false; + + // Editor defaults + this.jsValue = vnode.attrs.jsValue || ""; + this.jsonValue = vnode.attrs.jsonValue || "{}"; + + // Execute endpoint + this.executeUrl = vnode.attrs.executeUrl; + + // State for environment toggle, fetch results, etc. + this.showEnvironment = false; + this.loading = false; + this.error = null; + this.response = null; // JSON from server + this.responseRaw = ""; // Raw text from server + this.responseTime = 0; + this.responseSize = 0; + + // URL props + this.saveUrl = vnode.attrs.saveUrl; + + // Separate loading states for each button + this.executeLoading = false; + this.saveLoading = false; + + // New prop for showing/hiding delete button + this.showDeleteButton = vnode.attrs.showDeleteButton !== false; // default true + + // Delete endpoint + this.deleteUrl = vnode.attrs.deleteUrl; + + this.dashboardUrl = vnode.attrs.dashboardUrl; + }, + + oncreate() { + // Initialize top JS editor + this.editorJS = ace.edit("js-editor"); + this.editorJS.setOptions({ maxLines: 100 }); + this.editorJS.setTheme("ace/theme/github_dark"); + this.editorJS.session.setMode("ace/mode/javascript"); + this.editorJS.setValue(this.jsValue, -1); + + this.editorJS.session.on("change", () => { + this.jsValue = this.editorJS.getValue(); + m.redraw(); + }); + + // Initialize bottom JSON editor + this.editorJSON = ace.edit("json-editor"); + this.editorJSON.setOptions({ maxLines: 100 }); + this.editorJSON.setTheme("ace/theme/github_dark"); + this.editorJSON.session.setMode("ace/mode/json"); + this.editorJSON.setValue(this.jsonValue, -1); + + this.editorJSON.session.on("change", () => { + this.jsonValue = this.editorJSON.getValue(); + m.redraw(); + }); + }, + + async execute() { + this.executeLoading = true; + this.error = null; + this.response = null; + this.responseRaw = ""; + this.responseSize = 0; + this.responseTime = 0; + + const startTime = Date.now(); + try { + const code = this.editorJS.getValue(); + const environment_info = this.editorJSON.getValue(); + + const resp = await fetch(this.executeUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code, environment_info }), + }); + if (!resp.ok) { + throw new Error(`HTTP error! status: ${resp.status}`); + } + + this.responseRaw = await resp.text(); + this.responseSize = new Blob([this.responseRaw]).size; + this.response = JSON.parse(this.responseRaw); + this.responseTime = Date.now() - startTime; + } catch (err) { + this.error = err; + } finally { + this.executeLoading = false; + m.redraw(); + } + }, + + async save() { + this.saveLoading = true; + this.error = null; + + try { + const payload = { + name: this.name, + script_content: this.jsValue, + environment_info: this.jsonValue, + is_public: this.isPublic, + log_request: this.logRequest, + log_response: this.logResponse + }; + + const response = await m.request({ + method: 'POST', + url: this.saveUrl, + body: payload + }); + + if (response.status === 'success') { + if (this.isAdd) { + window.location.href = this.dashboardUrl; + } else { + // Increment version number after successful save + this.versionNumber = (parseInt(this.versionNumber) + 1).toString(); + } + Alert.show(response.message || 'Function saved successfully!', 'success'); + } else { + Alert.show(response.message || 'Error saving function', 'error'); + this.error = new Error(response.message); + } + } catch (err) { + Alert.show(err?.response.message || 'Error saving function', 'error'); + this.error = err?.response; + } finally { + this.saveLoading = false; + m.redraw(); + } + }, + + async delete() { + if (!confirm('Are you sure you want to delete this function?')) { + return; + } + + this.deleteLoading = true; + this.error = null; + + try { + const response = await m.request({ + method: 'DELETE', + url: this.deleteUrl + }); + + if (response.status === 'success') { + Alert.show(response.message || 'Function deleted successfully!', 'success'); + // Optionally redirect to a different page after deletion + window.location.href = '/dashboard/http_functions'; + } else { + Alert.show(response.message || 'Error deleting function', 'error'); + this.error = new Error(response.message); + } + } catch (err) { + Alert.show(err.message || 'Error deleting function', 'error'); + this.error = err; + } finally { + this.deleteLoading = false; + m.redraw(); + } + }, + + view() { + return m("div", { class: "" }, [ + /* ───────────────────────────────────────────────────────────────── + HEADER BAR + ─────────────────────────────────────────────────────────────────*/ + m( + "div", + { + class: + "flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800", + }, + [ + // Left side: name/version OR add input (shown only if showHeader==true) + this.showHeader + ? m("div", { class: "flex space-x-2" }, [ + // If editing existing function + this.isEdit + ? m( + "div", + { + class: + "inline-flex items-center space-x-1 h-10 py-2 text-gray-600 dark:text-gray-400 text-md font-medium cursor-pointer", + }, + [ + m("span", { class: "inline-flex items-center" }, [ + !this.nameEditing + ? m( + "span", + { + class: "font-mono", + onclick: () => (this.nameEditing = true), + }, + this.name + ) + : m("input", { + class: + "bg-gray-50 border border-gray-300 text-sm rounded-lg p-1.5 dark:bg-gray-700 dark:border-gray-600", + value: this.name, + oninput: (e) => (this.name = e.target.value), + onblur: () => (this.nameEditing = false), + autofocus: true, + }), + + // Pencil icon + m( + "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", + onclick: () => + (this.nameEditing = !this.nameEditing), + }, + m("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", + }) + ), + ]), + // Version + m( + "span", + { + class: + "bg-blue-500 text-white text-xs font-semibold px-2 py-1 rounded ml-2", + }, + `v${this.versionNumber}` + ), + ] + ) + : null, + + // If adding a new function + this.isAdd + ? m("div", { class: "w-full" }, [ + m( + "label", + { class: "block mb-2 text-sm font-medium" }, + "Function name" + ), + m("input", { + type: "text", + class: "bg-gray-50 border w-full p-2.5 rounded-lg", + placeholder: "foo", + required: true, + value: this.name, + oninput: (e) => (this.name = e.target.value), + }), + ]) + : null, + ]) + : m("div"), // If header is hidden, left side is empty + + // Right side: always show spinner or execute button + m("div", { class: "flex items-center space-x-3" }, [ + this.executeLoading + ? m("div", { + class: + "animate-spin h-6 w-6 border-4 border-green-300 border-t-transparent rounded-full", + }) + : m( + "button", + { + class: "p-2 rounded-full hover:bg-gray-200 text-green-700", + onclick: () => this.execute(), + title: "Execute", + }, + m( + "svg", + { + 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", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M5.25 5.25 19.5 12 5.25 18.75 5.25 5.25z", + }) + ) + ), + ]), + ] + ), + + /* ───────────────────────────────────────────────────────────────── + JS Editor + ─────────────────────────────────────────────────────────────────*/ + m("div", { id: "js-editor", class: "rounded shadow h-64" }), + + /* ───────────────────────────────────────────────────────────────── + Environment Toggle + ─────────────────────────────────────────────────────────────────*/ + m( + "div", + { class: "flex space-x-2 border-b border-gray-200 justify-between" }, + [ + m( + "button", + { + class: + "inline-flex items-center px-4 py-2 h-10 text-gray-600 hover:bg-accent hover:text-accent-foreground", + onclick: () => (this.showEnvironment = !this.showEnvironment), + }, + [ + m( + "svg", + { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-4 h-4", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125", + }) + ), + m("span", { class: "ml-1" }, "Environment"), + ] + ), + m("div", { class: "flex-auto" }), + ] + ), + + /* ───────────────────────────────────────────────────────────────── + JSON Editor + ─────────────────────────────────────────────────────────────────*/ + m("div", { id: "json-editor", class: "rounded shadow h-64" }), + + /* ───────────────────────────────────────────────────────────────── + Hidden fields (if needed for forms) + ─────────────────────────────────────────────────────────────────*/ + m("input", { type: "hidden", name: "script", value: this.jsValue }), + m("input", { + type: "hidden", + name: "environment", + value: this.jsonValue, + }), + + /* ───────────────────────────────────────────────────────────────── + Loading & Error + ─────────────────────────────────────────────────────────────────*/ + this.error && + m( + "div", + { class: "mt-2 p-2 text-red-600 font-semibold" }, + `Error: ${this.error.message}` + ), + + // Function settings panel + this.showFunctionSettings && m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [ + // Settings group + m("div", { class: "flex flex-col space-y-4" }, [ + // Toggles group + m("div", { class: "flex flex-wrap gap-6" }, [ + // Public/Private toggle + this.showPublicToggle && m("label", { + class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" + }, [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.isPublic, + onchange: (e) => this.isPublic = e.target.checked + }), + m("div", { + class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" + }) + ]), + m("span", "Public Function") + ]), + + // Log Request toggle + this.showLogRequestToggle && m("label", { + class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" + }, [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.logRequest, + onchange: (e) => this.logRequest = e.target.checked + }), + m("div", { + class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" + }) + ]), + m("span", "Log Requests") + ]), + + // Log Response toggle + this.showLogResponseToggle && m("label", { + class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" + }, [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.logResponse, + onchange: (e) => this.logResponse = e.target.checked + }), + m("div", { + class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" + }) + ]), + m("span", "Log Responses") + ]) + ]), + + // Actions group + m("div", { class: "flex items-center justify-end space-x-3 pt-2" }, [ + // Save button + this.showSaveButton && m("button", { + class: "px-4 py-2 bg-blue-500 text-white text-sm font-medium rounded-lg hover:bg-blue-600 transition-colors flex items-center space-x-2 disabled:opacity-50", + onclick: () => this.save(), + disabled: this.saveLoading + }, [ + this.saveLoading && m("div", { + class: "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" + }), + m("span", this.saveLoading ? "Saving..." : "Save Function") + ]), + + // Delete button + this.showDeleteButton && m("button", { + class: "px-4 py-2 bg-white text-red-600 text-sm font-medium border border-red-200 rounded-lg hover:bg-red-50 transition-colors flex items-center space-x-2 disabled:opacity-50", + onclick: () => this.delete(), + disabled: this.deleteLoading + }, [ + this.deleteLoading && m("div", { + class: "animate-spin h-4 w-4 border-2 border-red-600 border-t-transparent rounded-full" + }), + m("span", this.deleteLoading ? "Deleting..." : "Delete Function") + ]) + ]) + ]) + ]), + + /* ───────────────────────────────────────────────────────────────── + ResponseView (child) if needed + ─────────────────────────────────────────────────────────────────*/ + !this.executeLoading && + !this.error && + this.response && + m(ResponseView, { + response: this.response, + responseTime: this.responseTime, + responseSize: this.responseSize, + envEditorValue: this.jsonValue, + onClose: () => { + this.response = null; + }, + }), + ]); + }, +}; diff --git a/static/js/mithril/responseView.js b/static/js/mithril/responseView.js new file mode 100644 index 0000000..9646a2a --- /dev/null +++ b/static/js/mithril/responseView.js @@ -0,0 +1,358 @@ +const ResponseView = { + oninit() { + // Which tab is visible? 0=Logs, 1=Preview, 2=Raw, 3=Diff + this.tabIndex = 1; + }, + + view(vnode) { + const { + response, // Full JSON from server + responseTime, // #ms from request start to finish + responseSize, // #bytes in the raw response + envEditorValue, // The JSON environment from the editor (string) + onClose, // Callback to clear or close this view + } = vnode.attrs; + + // If there's no response, nothing to show + if (!response) return null; + + return m("div", { class: "mt-2 p-1 rounded-md bg-gray-200 min-h-40" }, [ + /* ───────────────────────────────────────────────────────────────── + TAB HEADERS + ─────────────────────────────────────────────────────────────────*/ + m("div", { class: "flex justify-between items-center" }, [ + // Left: 4 tab buttons + m( + "div", + { + class: "flex space-x-4 text-sm font-medium text-gray-600 ml-2 py-1", + }, + [ + // Logs (tabIndex=0) + m( + "div", + { + class: + "flex items-center space-x-1 cursor-pointer " + + (this.tabIndex === 0 + ? "border-b-2 text-blue-400 border-blue-400" + : ""), + onclick: () => (this.tabIndex = 0), + }, + [ + m( + "svg", + { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-4 h-4", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5", + }) + ), + m("span", `Logs(${response.logs.length})`), + ] + ), + + // Raw (tabIndex=2) + m( + "div", + { + class: + "flex items-center space-x-1 cursor-pointer " + + (this.tabIndex === 2 + ? "border-b-2 text-blue-400 border-blue-400" + : ""), + onclick: () => (this.tabIndex = 2), + }, + [ + m( + "svg", + { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-4 h-4", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M19.5 3 4.5 3m15 0v3.75c0 .621-.504 1.125-1.125 1.125h-3.75m4.875-4.875-4.875 4.875m-9.75 0A2.625 2.625 0 0 1 3 5.25v-.75c0-.621.504-1.125 1.125-1.125h.75c.621 0 1.125.504 1.125 1.125v.75c0 .621-.504 1.125-1.125 1.125H4.5Zm0 6.375h15m-15 6h15", + }) + ), + m("span", "Raw"), + ] + ), + + // Diff (tabIndex=3) + m( + "div", + { + class: + "flex items-center space-x-1 cursor-pointer " + + (this.tabIndex === 3 + ? "border-b-2 text-blue-400 border-blue-400" + : ""), + onclick: () => (this.tabIndex = 3), + }, + [ + m( + "svg", + { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-4 h-4", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M16.5 3.75 7.5 20.25m9-16.5H9m7.5 0V12m-9 8.25h9m-9 0V12", + }) + ), + m("span", "Diff"), + ] + ), + + // Preview (tabIndex=1) + m( + "div", + { + class: + "flex items-center space-x-1 cursor-pointer " + + (this.tabIndex === 1 + ? "border-b-2 text-blue-400 border-blue-400" + : ""), + onclick: () => (this.tabIndex = 1), + }, + [ + m( + "svg", + { + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + viewBox: "0 0 24 24", + "stroke-width": "1.5", + stroke: "currentColor", + class: "w-4 h-4", + }, + [ + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z", + }), + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z", + }), + ] + ), + m("span", "Preview"), + ] + ), + ] + ), + + // Right: Close icon + m( + "div", + { + class: "cursor-pointer text-gray-600 rounded-md hover:bg-gray-300", + onclick: onClose, + }, + m( + "svg", + { + 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", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "M6 18 18 6M6 6l12 12", + }) + ) + ), + ]), + + /* ───────────────────────────────────────────────────────────────── + TAB CONTENT + ─────────────────────────────────────────────────────────────────*/ + m("div", { class: "p-2" }, [ + // (0) LOGS + this.tabIndex === 0 && + m( + "div", + { class: "text-sm font-mono space-y-1" }, + (response.logs || []).map((log) => + m("div", [ + m( + "svg", + { + class: "inline-block w-4 h-4 mr-1", + fill: "none", + stroke: "currentColor", + "stroke-width": "1.5", + viewBox: "0 0 24 24", + }, + m("path", { + "stroke-linecap": "round", + "stroke-linejoin": "round", + d: "m8.25 4.5 7.5 7.5-7.5 7.5", + }) + ), + log, + ]) + ) + ), + + // (2) RAW + this.tabIndex === 2 && + m("div", { class: "space-y-2" }, [ + // Show raw body + m("div", { class: "bg-white p-2 rounded-md" }, [ + m("div", { class: "font-bold text-gray-600 mb-1" }, "Raw Body"), + m( + "pre", + { class: "text-sm overflow-auto" }, + response.result?.body ?? "No body" + ), + ]), + + // Show headers + m("div", { class: "bg-white p-2 rounded-md" }, [ + m("div", { class: "font-bold text-gray-600 mb-1" }, "Headers"), + response.result?.headers + ? m( + "table", + { class: "text-sm w-full border-collapse" }, + Object.entries(response.result.headers).map(([k, v]) => + m("tr", [ + m("td", { class: "border p-1 font-medium w-1/4" }, k), + m("td", { class: "border p-1" }, v), + ]) + ) + ) + : m("div", "No headers"), + ]), + ]), + + // (3) DIFF + this.tabIndex === 3 && + m("div", { + id: "env-diff-container", + style: "position: relative; width: 100%; height: 300px;", + oncreate: (vnode) => { + // Build "left" from envEditorValue + let leftText; + try { + // Attempt to format nicely + const leftObj = JSON.parse(envEditorValue); + leftText = JSON.stringify(leftObj, null, 2); + } catch (e) { + // Fallback to raw string + leftText = envEditorValue; + } + + // Build "right" from server environment + let rightText = ""; + let serverEnv = response?.environment || {}; + try { + rightText = JSON.stringify(serverEnv, null, 2); + } catch (err) { + rightText = String(serverEnv); + } + + // Initialize AceDiff + const aceDiffer = new AceDiff({ + element: vnode.dom, + mode: "ace/mode/json", // Or "ace/mode/javascript" + theme: "ace/theme/github_dark", + left: { + content: leftText, + editable: false, + }, + right: { + content: rightText, + editable: false, + }, + }); + + // Optional: set max lines, disable worker + const ed = aceDiffer.getEditors(); + ed.left.setOptions({ maxLines: 20 }); + ed.left.session.setOption("useWorker", false); + ed.right.setOptions({ maxLines: 20 }); + ed.right.session.setOption("useWorker", false); + }, + }), + + // (1) PREVIEW + this.tabIndex === 1 && + m( + "div", + { + class: + "min-h-32 rounded-md bg-white p-2 " + + (response.status !== "SUCCESS" + ? "border-red-700 border-2" + : ""), + }, + response.status === "SUCCESS" + ? m.trust(response.result.body) + : JSON.stringify(response.result) + ), + ]), + + /* ───────────────────────────────────────────────────────────────── + FOOTER (Status, Time, Size) + ─────────────────────────────────────────────────────────────────*/ + m("div", { class: "flex justify-end p-1" }, [ + m("div", { class: "text-sm font-medium text-gray-600 space-x-4" }, [ + // Status + m("span", [ + "Status: ", + m( + "span", + { + class: + response.status === "SUCCESS" + ? "text-green-600" + : "text-red-700", + }, + response?.result?.status || "Error" + ), + ]), + // Time + responseTime != null && + m("span", [ + "Time: ", + m("span", { class: "text-green-600" }, `${responseTime}ms`), + ]), + // Size + responseSize != null && + m("span", [ + "Size: ", + m("span", { class: "text-green-600" }, `${responseSize} bytes`), + ]), + ]), + ]), + ]); + }, +}; diff --git a/templates/base.html b/templates/base.html index 2df5f65..d78c9fa 100644 --- a/templates/base.html +++ b/templates/base.html @@ -23,6 +23,10 @@ crossorigin="anonymous" referrerpolicy="no-referrer"> + + + + diff --git a/templates/dashboard/http_functions/client.html b/templates/dashboard/http_functions/client.html index 8ba5c30..b7510e1 100644 --- a/templates/dashboard/http_functions/client.html +++ b/templates/dashboard/http_functions/client.html @@ -2,9 +2,14 @@ {% block page %} -{{ render_partial('dashboard/http_functions/header.html', title='Try', user_id=user_id, function_id=function_id, -name=name, -show_refresh=False, show_link=False, show_edit_form=True, show_client=True, show_logs=True, show_history=True) }} +{{ 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_function_editor', function_id=function_id), +cancel_url=url_for('dashboard_http_functions')) }}
diff --git a/templates/dashboard/http_functions/edit.html b/templates/dashboard/http_functions/edit.html deleted file mode 100644 index 6cb9777..0000000 --- a/templates/dashboard/http_functions/edit.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'dashboard.html' %} - -{% block page %} - -{{ render_partial('dashboard/http_functions/header.html', title='Update', user_id=user_id, function_id=function_id, -name=name, -refresh_url=url_for('get_http_function_edit_form', function_id=function_id), show_logs=True, show_client=True, -show_history=True) }} - -{{ render_partial('function_editor.html', function_id=function_id, name=name, script=script, -environment_info=environment_info, -is_public=is_public, log_request=log_request, log_response=log_response, version_number=version_number, is_edit=True) }} - -{% endblock %} \ No newline at end of file diff --git a/templates/dashboard/http_functions/editor.html b/templates/dashboard/http_functions/editor.html new file mode 100644 index 0000000..31c43d2 --- /dev/null +++ b/templates/dashboard/http_functions/editor.html @@ -0,0 +1,41 @@ +{% extends 'dashboard.html' %} + +{% block page %} + +{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id, +active_tab='edit', +show_edit_form=True, +show_logs=True, +show_client=True, +show_history=True, +edit_url=edit_url, +cancel_url=cancel_url) }} + + +
+ +
+ + + +{% endblock %} \ No newline at end of file diff --git a/templates/dashboard/http_functions/header.html b/templates/dashboard/http_functions/header.html index d39efb1..8094a32 100644 --- a/templates/dashboard/http_functions/header.html +++ b/templates/dashboard/http_functions/header.html @@ -1,103 +1,108 @@ -
-
-
+
+ +
+ +

- {{ title }}{% if show_name|default(true, false) %}: {{ name }}{% endif %} + {{ title }}

- - {% if show_refresh|default(true, false) %} -
- - - - -
+ {% if show_edit_form|default(false, true) %} + {% endif %} {% if show_logs|default(false, true) %} -
- - - -
+
+ + + +
+ Logs + {% endif %} {% if show_client|default(false, true) %} -
- - - -
- {% endif %} - - {% if show_edit_form|default(false, true) %} -
- - - -
+ {% endif %} {% if show_history|default(false, true) %} -
- - - - -
+
+ + + + +
+ History + {% endif %} + {% if show_new|default(false, true) %} + + {% endif %}
-
- {% if show_link|default(true, false) %} - - {% endif %}
\ No newline at end of file diff --git a/templates/dashboard/http_functions/history.html b/templates/dashboard/http_functions/history.html index e147fa6..55578d3 100644 --- a/templates/dashboard/http_functions/history.html +++ b/templates/dashboard/http_functions/history.html @@ -2,10 +2,14 @@ {% block page %} -{{ render_partial('dashboard/http_functions/header.html', title='History', user_id=user_id, function_id=function_id, -name=name, -refresh_url=url_for('get_http_function_history', function_id=function_id), show_logs=True, show_client=True, -show_edit_form=True) }} +{{ render_partial('dashboard/http_functions/header.html', user_id=user_id, function_id=function_id, +active_tab='history', +show_edit_form=True, +show_logs=True, +show_client=True, +show_history=True, +edit_url=url_for('http_function_editor', function_id=function_id), +cancel_url=url_for('dashboard_http_functions')) }}
@@ -185,7 +189,6 @@ show_edit_form=True) }} }); editor.setTheme("ace/theme/github_dark"); editor.session.setMode("ace/mode/javascript"); - diff --git a/templates/dashboard/http_functions/logs.html b/templates/dashboard/http_functions/logs.html index c57a8c6..72ab1af 100644 --- a/templates/dashboard/http_functions/logs.html +++ b/templates/dashboard/http_functions/logs.html @@ -2,10 +2,14 @@ {% block page %} -{{ render_partial('dashboard/http_functions/header.html', title='Logs', user_id=user_id, function_id=function_id, -name=name, -refresh_url=url_for('get_http_function_logs', function_id=function_id), show_edit_form=True, show_client=True, -show_history=True) }} +{{ 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, +show_history=True, +edit_url=url_for('http_function_editor', function_id=function_id), +cancel_url=url_for('dashboard_http_functions')) }}
diff --git a/templates/dashboard/http_functions/new.html b/templates/dashboard/http_functions/new.html index c57bef4..2b6794a 100644 --- a/templates/dashboard/http_functions/new.html +++ b/templates/dashboard/http_functions/new.html @@ -2,12 +2,40 @@ {% block page %} -{{ render_partial('dashboard/http_functions/header.html', title='New HTTP function', user_id=user_id, show_name=False, -show_refresh=False, show_logs=False, -show_client=False, show_link=False) +{{ render_partial('dashboard/http_functions/header.html', +user_id=user_id, +show_name=False, +show_refresh=False, +show_logs=False, +show_client=False, +show_link=False, +dashboardUrl=url_for('dashboard_http_functions'), +title='New HTTP Function') }} -{{ render_partial('function_editor.html', name=name, script=script, environment_info=environment_info, -is_public=is_public, log_request=log_request, log_response=log_response, is_add=True) }} +
+ +
+ + {% endblock %} \ No newline at end of file diff --git a/templates/dashboard/http_functions/overview.html b/templates/dashboard/http_functions/overview.html index 7dfb2ec..483e56c 100644 --- a/templates/dashboard/http_functions/overview.html +++ b/templates/dashboard/http_functions/overview.html @@ -80,7 +80,7 @@ diff --git a/templates/home.html b/templates/home.html index d2ce5a9..9a33142 100644 --- a/templates/home.html +++ b/templates/home.html @@ -21,6 +21,13 @@ integrity="sha512-g9yptARGYXbHR9r3kTKIAzF+vvmgEieTxuuUUcHC5tKYFpLR3DR+lsisH2KZJG2Nwaou8jjYVRdbbbBQI3Bo5w==" crossorigin="anonymous" referrerpolicy="no-referrer"> + + + + + + +