<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="/webapp/favicon.png">
<title>
Cratery -- Workers nodes
</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<header style="position: sticky; top: 0;">
<nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
<div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
<a href="/webapp/index.html" class="flex items-center">
<picture>
<source srcset="./logo-white.svg" media="(prefers-color-scheme: dark)" />
<source srcset="./logo-black.svg" media="(prefers-color-scheme: light)" />
<img src="./logo-white.svg" class="mr-3 h-6 sm:h-9" style="min-width: 200px;" alt="Cratery Logo" />
</picture>
</a>
<div class="flex items-center lg:order-2">
<a id="link-admin" href="/webapp/admin.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Admin</a>
<a id="link-account" href="/webapp/account.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">My Account</a>
<a onclick="doLogout()" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Logout</a>
</div>
</div>
</nav>
</header>
<body onload="doPageLoad()" class="bg-white dark:bg-gray-800">
<section class="bg-white dark:bg-gray-900">
<div class="p-2 flex flex-row flex-wrap">
<a href="/webapp/admin.html" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" style="display: inline-block;">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
</svg>
Back to admin
</a>
</div>
<div class="py-4 lg:py-4 px-4 mx-auto max-w-screen-xxl">
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">Worker nodes</h2>
<div class="relative overflow-x-auto space-y-8">
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Node
</th>
<th scope="col" class="px-6 py-3">
Host
</th>
<th scope="col" class="px-6 py-3">
Versions
</th>
<th scope="col" class="px-6 py-3">
Installed targets
</th>
<th scope="col" class="px-6 py-3">
Capabilities
</th>
<th scope="col" class="px-6 py-3">
Status
</th>
</tr>
</thead>
<tbody id="workers">
</tbody>
</table>
</div>
</div>
</section>
</body>
<footer class="p-4 bg-white md:p-8 lg:p-10 dark:bg-gray-800">
<div class="mx-auto max-w-screen-xl text-center">
<span class="text-sm text-gray-500 sm:text-center dark:text-gray-400">Version <span id="version"></span>, Copyright © <span id="year"></span> <a href="https://cenotelie.fr/" target="_blank" class="hover:underline">Cénotélie</a>. All Rights Reserved.</span>
</div>
</footer>
<link href="/webapp/index.css" rel="stylesheet" />
<script src="/webapp/api.js"></script>
<script src="/webapp/index.js"></script>
<script src="/webapp/stream.js"></script>
<script>
function doPageLoad() {
onPageLoad().then((_user) => {
apiGetWorkers().then((workers) => {
const table = document.getElementById("workers");
for (const worker of workers) {
table.appendChild(renderWorker(worker));
}
});
const sourceForWorkers = new StreamEventSource("/api/v1/admin/workers/updates", null);
sourceForWorkers.map(message => JSON.parse(message.data)).then((event) => onWorkerEvent(event)).catch(() => {});
});
}
function renderWorker(worker) {
const row = document.createElement("tr");
row.className = "bg-white border-b dark:bg-gray-800 dark:border-gray-700";
row.id = `worker-${worker.descriptor.identifier}`;
const cell1 = document.createElement("th");
cell1.setAttribute("scope", "row");
cell1.className = "px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white";
cell1.appendChild(document.createTextNode(`${worker.descriptor.name} (${worker.descriptor.identifier})`));
const cell2 = document.createElement("td");
cell2.className = "px-6 py-4";
cell2.appendChild(document.createTextNode(worker.descriptor.toolchainHost));
const cell3 = document.createElement("td");
cell3.className = "px-6 py-4";
cell3.appendChild(document.createTextNode(`${worker.descriptor.toolchainVersionStable}, ${worker.descriptor.toolchainVersionNightly }`));
const cell4 = document.createElement("td");
cell4.className = "px-6 py-4";
for (const target of worker.descriptor.toolchainInstalledTargets) {
const el = document.createElement("div");
el.appendChild(document.createTextNode(target));
cell4.appendChild(el);
}
const cell5 = document.createElement("td");
cell5.className = "px-6 py-4";
for (const target of worker.descriptor.capabilities) {
const el = document.createElement("div");
el.appendChild(document.createTextNode(target));
cell5.appendChild(el);
}
const cell6 = document.createElement("td");
cell6.className = "px-6 py-4";
cell6.id = `worker-state-${worker.descriptor.identifier}`;
cell6.appendChild(renderWorkerStatus(worker.state));
row.appendChild(cell1);
row.appendChild(cell2);
row.appendChild(cell3);
row.appendChild(cell4);
row.appendChild(cell5);
row.appendChild(cell6);
return row;
}
function renderWorkerStatus(state) {
const color = state === "Available" ? "green" : "blue";
const statusEl = document.createElement("span");
statusEl.className = `bg-${color}-100 text-${color}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-${color}-900 dark:text-${color}-300`;
statusEl.style.display = "inline-block";
statusEl.appendChild(document.createTextNode(state === "Available" ? "Available" : "Working ..."));
return statusEl;
}
function onWorkerEvent(event) {
const keys = Object.getOwnPropertyNames(event);
if (keys.includes("WorkerConnected")) {
const table = document.getElementById("workers");
table.appendChild(renderWorker(event.WorkerConnected));
} else if (keys.includes("WorkerRemoved")) {
const row = document.getElementById(`worker-${event.WorkerRemoved.worker_id}`);
row.remove();
} else if (keys.includes("WorkerStartedJob")) {
const cell6 = document.getElementById(`worker-state-${event.WorkerStartedJob.worker_id}`);
removeAllChildren(cell6);
cell6.appendChild(renderWorkerStatus("InUse"));
} else if (keys.includes("WorkerAvailable")) {
const cell6 = document.getElementById(`worker-state-${event.WorkerAvailable.worker_id}`);
removeAllChildren(cell6);
cell6.appendChild(renderWorkerStatus("Available"));
}
}
</script>
</html>