Refactor HTTP function editor with Mithril components and new API endpoints

- Add new Mithril components for editor (editor.js), response view (responseView.js), and alerts (alert.js)
- Create new API endpoints for creating, updating, and deleting HTTP functions
- Update templates to use new Mithril editor component
- Improve header navigation with more consistent styling and active state indicators
- Remove old edit form route and template
- Add new dedicated editor route and template

cursor.ai
This commit is contained in:
Peter Stockings
2025-02-14 00:40:45 +11:00
parent abf3ca15d2
commit 5ec8bba9e8
15 changed files with 1230 additions and 131 deletions

View File

@@ -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 = `
<div class="flex items-center">
<span class="mr-2">
${type === 'success' ? `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
` : `
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
`}
</span>
<span>${message}</span>
</div>
`;
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);
}
};

496
static/js/mithril/editor.js Normal file
View File

@@ -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;
},
}),
]);
},
};

View File

@@ -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`),
]),
]),
]),
]);
},
};