Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions src/Components/HMI/ui/public/admin/api-explorer.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<!doctype html>
<html lang="en">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Echo Admin – API Explorer</title>

<link rel="shortcut icon" type="image/png" href="/admin/images/logos/favicon.png" />
<link type="text/css" rel="stylesheet" href="/admin/css/styles.min.css" />

<!-- Bootstrap -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
rel="stylesheet"
/>

<!-- Icons -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css"
/>

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

<!-- Sidebar + App JS -->
<script src="/admin/js/sidebarmenu.js"></script>
<script src="/admin/js/app.min.js"></script>

<!-- Simplebar -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css" />
<script src="https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js"></script>
</head>

<body>
<!-- Page Wrapper -->
<div
class="page-wrapper"
id="main-wrapper"
data-layout="vertical"
data-navbarbg="skin6"
data-sidebartype="full"
data-sidebar-position="fixed"
data-header-position="fixed"
>
<!-- Sidebar -->
<div id="sidebar"></div>

<!-- Main Content -->
<div class="body-wrapper">
<div id="header"></div>

<div class="container-fluid">

<!-- Header Row -->
<div class="d-flex align-items-start justify-content-between flex-wrap gap-2 mb-3">
<div>
<h5 class="fw-semibold mb-1">API Explorer</h5>
<div class="text-muted">
Test API endpoints and check their status / health.
</div>
</div>

<div class="text-end">
<span class="badge bg-secondary" id="statusBadge">Status: not checked</span>
<div class="text-muted small mt-1" id="lastChecked">Last checked: —</div>
<div class="text-muted small" id="respTime">Response time: —</div>
</div>
</div>

<!-- Main Card -->
<div class="card">
<div class="card-body">

<!-- Endpoint Builder -->
<div class="row g-3 mb-3">
<div class="col-12 col-lg-3">
<label class="form-label">Start (epoch)</label>
<input type="text" class="form-control" id="startTs" value="1763654400" />
</div>

<div class="col-12 col-lg-3">
<label class="form-label">End (epoch)</label>
<input type="text" class="form-control" id="endTs" value="1763740800" />
</div>

<div class="col-12 col-lg-2 d-grid">
<label class="form-label invisible">Build</label>
<button class="btn btn-outline-primary" id="buildBtn">
Build Endpoint
</button>
</div>

<div class="col-12 col-lg-4">
<label class="form-label">Endpoint</label>
<input
type="text"
class="form-control"
id="endpoint"
value="/movement_time/1763654400/1763740800"
/>
</div>
</div>

<!-- Action Buttons -->
<div class="d-flex gap-2 flex-wrap mb-3">
<button class="btn btn-primary" id="testBtn">Test</button>
<button class="btn btn-outline-secondary" id="clearBtn">Clear Output</button>
</div>

<!-- Content Row -->
<div class="row g-3">

<!-- Endpoint List -->
<div class="col-12 col-lg-4">
<div class="fw-semibold mb-2">Available now</div>
<div class="list-group mb-3">
<button
class="list-group-item list-group-item-action"
data-ep="/movement_time/1763654400/1763740800"
>
GET /movement_time/&lt;start&gt;/&lt;end&gt;
<span class="badge bg-success ms-2">known</span>
</button>
</div>

<div class="fw-semibold mb-2">Coming next sprint</div>
<div class="list-group">
<button class="list-group-item list-group-item-action" data-ep="/health">
GET /health
<span class="badge bg-secondary ms-2">placeholder</span>
</button>
<button class="list-group-item list-group-item-action" data-ep="/nodes">
GET /nodes
<span class="badge bg-secondary ms-2">placeholder</span>
</button>
<button class="list-group-item list-group-item-action" data-ep="/devices">
GET /devices
<span class="badge bg-secondary ms-2">placeholder</span>
</button>
</div>
</div>

<!-- Response -->
<div class="col-12 col-lg-8">
<div class="fw-semibold mb-2">Response</div>
<pre
class="p-3 bg-light rounded border"
id="output"
style="min-height: 320px; overflow:auto;"
>Click "Test" to send a request.</pre>
</div>

</div>
</div>
</div>

</div>
</div>
</div>

<!-- Load sidebar / header / footer -->
<script type="text/javascript">
$(document).ready(function () {
$("#sidebar").load("/admin/component/sidebar-component.html", function () {
$.getScript("/admin/js/sidebarmenu.js");
});
$("#header").load("/admin/component/header-component.html");
$("#footer").load("/admin/component/footer-component.html");
});
</script>

<!-- Page Logic -->
<script src="./js/api-explorer.js"></script>

<div id="footer"></div>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
<li>
<a href="/admin/cloud-compute.html" style="display:block;padding:0 16px;height:40px;line-height:40px;color:#1F2D23;text-decoration:none;font-weight:500;font-size:14px;">Cloud Compute</a>
</li>
<li>
<a href="/admin/api-explorer.html"
style="display:block;padding:0 16px;height:40px;line-height:40px;color:#1F2D23;text-decoration:none;font-weight:500;font-size:14px;">
API Explorer
</a>
</li>

<li>
<a href="/admin/projects.html" style="display:block;padding:0 16px;height:40px;line-height:40px;color:#1F2D23;text-decoration:none;font-weight:500;font-size:14px;">Project management</a>
</li>
Expand Down
99 changes: 99 additions & 0 deletions src/Components/HMI/ui/public/admin/js/api-explorer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
(() => {
const endpointEl = document.getElementById("endpoint");
const testBtn = document.getElementById("testBtn");
const buildBtn = document.getElementById("buildBtn");
const clearBtn = document.getElementById("clearBtn");

const startTs = document.getElementById("startTs");
const endTs = document.getElementById("endTs");

const output = document.getElementById("output");
const statusBadge = document.getElementById("statusBadge");
const lastChecked = document.getElementById("lastChecked");
const respTime = document.getElementById("respTime");

function setBadge(type, text) {
statusBadge.textContent = text;
statusBadge.className = "badge " + type;
}

function buildMovementTimeEndpoint() {
const start = (startTs.value || "").trim();
const end = (endTs.value || "").trim();
endpointEl.value = `/movement_time/${start}/${end}`;
}

// Build button
buildBtn?.addEventListener("click", () => {
buildMovementTimeEndpoint();
});

// Clear output
clearBtn?.addEventListener("click", () => {
output.textContent = "Click \"Test\" to send a request.";
setBadge("bg-secondary", "Status: not checked");
lastChecked.textContent = "Last checked: —";
respTime.textContent = "Response time: —";
});

// Clicking an endpoint from lists fills the input
document.querySelectorAll("[data-ep]").forEach((btn) => {
btn.addEventListener("click", () => {
const ep = btn.getAttribute("data-ep");
endpointEl.value = ep;

// If user clicked movement_time placeholder, also rebuild from inputs
if (ep && ep.startsWith("/movement_time/") && startTs && endTs) {
buildMovementTimeEndpoint();
}
});
});

testBtn.addEventListener("click", async () => {
const endpoint = (endpointEl.value || "").trim();
lastChecked.textContent = "Last checked: " + new Date().toLocaleString();
respTime.textContent = "Response time: —";
output.textContent = `Sending GET ${endpoint}...\n`;

// Build a safe URL to avoid relative path issues
const url = endpoint.startsWith("http")
? endpoint
: `${window.location.origin}${endpoint.startsWith("/") ? "" : "/"}${endpoint}`;

try {
setBadge("bg-warning", "Status: testing...");

const t0 = performance.now();
const res = await fetch(url, { method: "GET" });
const t1 = performance.now();
const ms = Math.round(t1 - t0);

respTime.textContent = `Response time: ${ms} ms`;

const contentType = res.headers.get("content-type") || "";
const raw = await res.text();

setBadge(res.ok ? "bg-success" : "bg-danger", `Status: ${res.status}`);

let body = raw;
if (contentType.includes("application/json")) {
try {
body = JSON.stringify(JSON.parse(raw), null, 2);
} catch (e) {}
}

output.textContent =
`Request: GET ${url}\n` +
`HTTP Status: ${res.status}\n` +
`Content-Type: ${contentType || "unknown"}\n\n` +
`Response:\n${body}`;
} catch (err) {
setBadge("bg-danger", "Status: failed");
output.textContent =
`Request: GET ${url}\n\n` +
`Error:\n${err?.message || String(err)}`;
}
});
})();


7 changes: 7 additions & 0 deletions src/Components/HMI/ui/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,13 @@ app.get("/admin-nodes-temp", (req, res) => {
app.get("/admin-compute", (req, res) => {
return res.sendFile(path.join(__dirname, 'public/admin/cloud-compute.html'));
});
//api-explorer page
app.get("/admin-api-explorer", (req, res) => {
return res.sendFile(
path.join(__dirname, "public/admin/api-explorer.html")
);
});


app.get("/admin-projects", (req, res) => {
return res.sendFile(path.join(__dirname, 'public/admin/projects.html'));
Expand Down
Loading