|
// retoor <retoor@molodetz.nl>
|
|
|
|
import { CodeBlock } from "./CodeBlock.js";
|
|
import { devrantSession } from "./DevRantSession.js";
|
|
|
|
function el(tag, props = {}, children = []) {
|
|
const node = document.createElement(tag);
|
|
for (const [key, value] of Object.entries(props)) {
|
|
if (key === "class") node.className = value;
|
|
else if (key === "text") node.textContent = value;
|
|
else if (key === "html") node.innerHTML = value;
|
|
else if (value === true) node.setAttribute(key, "");
|
|
else if (value !== false && value !== null) node.setAttribute(key, value);
|
|
}
|
|
for (const child of children) {
|
|
if (child) node.appendChild(child);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
export class DevRantTester {
|
|
constructor(mount) {
|
|
this.mount = mount;
|
|
try {
|
|
this.config = JSON.parse(mount.dataset.config || "{}");
|
|
} catch {
|
|
return;
|
|
}
|
|
this.base = (window.DEVPLACE_DOCS || {}).base || location.origin;
|
|
this.inputs = [];
|
|
this.build();
|
|
this.renderExpected();
|
|
if (this.requiresAuth()) {
|
|
devrantSession.onChange(() => this.syncAuth());
|
|
this.syncAuth();
|
|
}
|
|
}
|
|
|
|
requiresAuth() {
|
|
return this.config.auth === "user";
|
|
}
|
|
|
|
build() {
|
|
const root = el("div", { class: "api-tester" });
|
|
if (this.config.params.length) root.appendChild(this.buildParams());
|
|
if (this.requiresAuth()) {
|
|
this.authNote = el("div", { class: "param-desc devrant-auth-note" });
|
|
root.appendChild(this.authNote);
|
|
}
|
|
root.appendChild(this.buildActions());
|
|
root.appendChild(this.buildResponse());
|
|
this.mount.appendChild(root);
|
|
}
|
|
|
|
buildParams() {
|
|
const table = el("div", { class: "param-table" });
|
|
for (const param of this.config.params) {
|
|
const control = this.buildControl(param);
|
|
this.inputs.push({ param, control });
|
|
const label = el("div", { class: "param-label" }, [
|
|
el("span", { class: "param-name", text: param.name }),
|
|
param.required ? el("span", { class: "param-required", text: "*" }) : null,
|
|
el("span", { class: "param-loc param-loc-" + param.location, text: param.location }),
|
|
]);
|
|
const meta = el("div", { class: "param-meta" }, [
|
|
control,
|
|
param.description ? el("div", { class: "param-desc", text: param.description }) : null,
|
|
]);
|
|
table.appendChild(el("div", { class: "param-row" }, [label, meta]));
|
|
}
|
|
return table;
|
|
}
|
|
|
|
buildControl(param) {
|
|
if (param.type === "enum") {
|
|
const select = el("select", { class: "param-input" });
|
|
for (const option of param.options || []) {
|
|
const opt = el("option", { value: option, text: option });
|
|
if (option === param.example) opt.selected = true;
|
|
select.appendChild(opt);
|
|
}
|
|
return select;
|
|
}
|
|
if (param.type === "textarea") {
|
|
const area = el("textarea", { class: "param-input", rows: "3" });
|
|
area.value = param.example || "";
|
|
return area;
|
|
}
|
|
const input = el("input", {
|
|
class: "param-input",
|
|
type: param.type === "int" ? "number" : param.type === "password" ? "password" : "text",
|
|
placeholder: param.example || "",
|
|
});
|
|
if (param.type !== "password") input.value = param.example || "";
|
|
return input;
|
|
}
|
|
|
|
buildActions() {
|
|
const bar = el("div", { class: "try-panel" });
|
|
this.sendBtn = el("button", { type: "button", class: "btn btn-primary try-send", text: "Send request" });
|
|
this.sendBtn.addEventListener("click", () => this.send());
|
|
bar.appendChild(this.sendBtn);
|
|
return bar;
|
|
}
|
|
|
|
syncAuth() {
|
|
if (!this.authNote) return;
|
|
if (devrantSession.isLoggedIn()) {
|
|
this.authNote.textContent = `Authenticated as ${devrantSession.username || "user " + devrantSession.auth.user_id}.`;
|
|
this.sendBtn.disabled = false;
|
|
} else {
|
|
this.authNote.textContent = "Requires login - use the Log in widget at the top of the page.";
|
|
this.sendBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
buildResponse() {
|
|
const wrap = el("div", { class: "response-viewer" });
|
|
const tabs = el("div", { class: "response-tabs" });
|
|
this.responseTabs = {};
|
|
for (const tab of [{ id: "expected", label: "Expected" }, { id: "live", label: "Live response" }]) {
|
|
const btn = el("button", { type: "button", class: "code-tab", text: tab.label });
|
|
btn.addEventListener("click", () => this.selectTab(tab.id));
|
|
this.responseTabs[tab.id] = btn;
|
|
tabs.appendChild(btn);
|
|
}
|
|
this.expectedEl = el("div", { class: "response-pane" });
|
|
this.liveEl = el("div", { class: "response-pane" });
|
|
this.liveEl.appendChild(el("p", { class: "response-placeholder", text: "Run the request to see the live response." }));
|
|
wrap.appendChild(tabs);
|
|
wrap.appendChild(this.expectedEl);
|
|
wrap.appendChild(this.liveEl);
|
|
this.selectTab("expected");
|
|
return wrap;
|
|
}
|
|
|
|
selectTab(id) {
|
|
for (const [name, btn] of Object.entries(this.responseTabs)) {
|
|
btn.classList.toggle("active", name === id);
|
|
}
|
|
this.expectedEl.classList.toggle("active", id === "expected");
|
|
this.liveEl.classList.toggle("active", id === "live");
|
|
}
|
|
|
|
renderExpected() {
|
|
const sample = this.config.sample_response;
|
|
if (sample === null || sample === undefined) {
|
|
this.expectedEl.appendChild(el("p", { class: "response-note", text: "No documented response body." }));
|
|
return;
|
|
}
|
|
this.expectedEl.appendChild(el("p", { class: "response-note", text: "Modeled JSON response:" }));
|
|
const pre = el("pre", { class: "response-body" });
|
|
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(sample, null, 2) }));
|
|
this.expectedEl.appendChild(pre);
|
|
CodeBlock.enhance(pre, { lineNumbers: false });
|
|
}
|
|
|
|
values() {
|
|
const out = { path: {}, query: {}, body: {} };
|
|
for (const { param, control } of this.inputs) {
|
|
const value = control.value;
|
|
if (value === "" && !param.required) continue;
|
|
out[param.location][param.name] = value;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
buildUrl(values) {
|
|
let path = this.config.path;
|
|
for (const [name, value] of Object.entries(values.path)) {
|
|
path = path.replace("{" + name + "}", encodeURIComponent(value));
|
|
}
|
|
const query = { ...values.query };
|
|
const usesQuery = this.config.method === "GET" || this.config.method === "DELETE";
|
|
if (usesQuery && this.requiresAuth()) Object.assign(query, devrantSession.triple());
|
|
const entries = Object.entries(query);
|
|
const qs = entries.length
|
|
? "?" + entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&")
|
|
: "";
|
|
return this.base + path + qs;
|
|
}
|
|
|
|
async confirmDestructive() {
|
|
if (!(this.config.destructive || this.config.method === "DELETE")) return true;
|
|
if (window.app && window.app.dialog) {
|
|
return window.app.dialog.confirm({
|
|
title: "Run request",
|
|
message: "This performs a real action on your account. Continue?",
|
|
confirmLabel: "Run",
|
|
danger: true,
|
|
});
|
|
}
|
|
return window.confirm("This performs a real action on your account. Continue?");
|
|
}
|
|
|
|
async send() {
|
|
if (this.requiresAuth() && !devrantSession.isLoggedIn()) {
|
|
this.showError("Log in first using the widget at the top of the page.");
|
|
return;
|
|
}
|
|
if (!(await this.confirmDestructive())) return;
|
|
const values = this.values();
|
|
const url = this.buildUrl(values);
|
|
const headers = { Accept: "application/json" };
|
|
const options = { method: this.config.method, headers };
|
|
if (this.config.encoding === "form") {
|
|
const body = { ...values.body };
|
|
if (this.requiresAuth()) Object.assign(body, devrantSession.triple());
|
|
options.body = new URLSearchParams(body).toString();
|
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
}
|
|
this.sendBtn.disabled = true;
|
|
const started = performance.now();
|
|
try {
|
|
const response = await fetch(url, options);
|
|
const ms = Math.round(performance.now() - started);
|
|
await this.renderResponse(response, ms, values);
|
|
} catch (error) {
|
|
this.showError(error.message || "Network error");
|
|
} finally {
|
|
this.sendBtn.disabled = false;
|
|
if (this.requiresAuth()) this.syncAuth();
|
|
}
|
|
}
|
|
|
|
async renderResponse(response, ms, values) {
|
|
const contentType = response.headers.get("content-type") || "";
|
|
this.liveEl.innerHTML = "";
|
|
this.selectTab("live");
|
|
const cls = "response-" + Math.floor(response.status / 100) + "xx";
|
|
const meta = el("div", { class: "response-meta" }, [
|
|
el("span", { class: "response-status " + cls, text: `${response.status} ${response.statusText}` }),
|
|
el("span", { text: `${ms} ms` }),
|
|
el("span", { text: contentType.split(";")[0] || "unknown" }),
|
|
]);
|
|
this.liveEl.appendChild(meta);
|
|
if (contentType.includes("application/json")) {
|
|
const data = await response.json();
|
|
this.maybeCaptureLogin(data, values);
|
|
const pre = el("pre", { class: "response-body" });
|
|
pre.appendChild(el("code", { class: "language-json", text: JSON.stringify(data, null, 2) }));
|
|
this.liveEl.appendChild(pre);
|
|
CodeBlock.enhance(pre, { lineNumbers: false });
|
|
return;
|
|
}
|
|
const text = await response.text();
|
|
const pre = el("pre", { class: "response-body" });
|
|
pre.appendChild(el("code", { text: text.slice(0, 2000) }));
|
|
this.liveEl.appendChild(pre);
|
|
CodeBlock.enhance(pre, { highlight: false, lineNumbers: false });
|
|
}
|
|
|
|
maybeCaptureLogin(data, values) {
|
|
const loginIds = ["devrant-login", "devrant-register"];
|
|
if (loginIds.includes(this.config.id) && data && data.success && data.auth_token) {
|
|
devrantSession.setFromToken(data.auth_token, values.body.username || "");
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
this.liveEl.innerHTML = "";
|
|
this.selectTab("live");
|
|
this.liveEl.appendChild(el("div", { class: "response-meta" }, [
|
|
el("span", { class: "response-status response-5xx", text: "Error" }),
|
|
]));
|
|
this.liveEl.appendChild(el("p", { class: "response-redirect", text: message }));
|
|
}
|
|
}
|