guix-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

01/01: hydra: Add Cuirass JavaScript front-end.


From: Ludovic Courtès
Subject: 01/01: hydra: Add Cuirass JavaScript front-end.
Date: Mon, 29 Jan 2018 12:18:28 -0500 (EST)

civodul pushed a commit to branch master
in repository maintenance.

commit cf6fc7ddfbf3644f813687dd5f3c0c244d75bfb0
Author: Danny Milosavljevic <address@hidden>
Date:   Mon Jan 29 18:17:48 2018 +0100

    hydra: Add Cuirass JavaScript front-end.
    
    This will be available as <https://berlin.guixsd.org/status>.
    
    * hydra/nginx/html/status/index.html: New file.
---
 hydra/nginx/html/status/index.html | 667 +++++++++++++++++++++++++++++++++++++
 1 file changed, 667 insertions(+)

diff --git a/hydra/nginx/html/status/index.html 
b/hydra/nginx/html/status/index.html
new file mode 100644
index 0000000..d95c599
--- /dev/null
+++ b/hydra/nginx/html/status/index.html
@@ -0,0 +1,667 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Cuirass Status Frontend</title>
+<style media="screen">
+<!--
+table {
+       border-collapse: collapse;
+       width: 90%;
+}
+table thead tr {
+       border-top: none 0px;
+       border-left: none 0px;
+       border-right: none 0px;
+       border-bottom: solid 1px;
+}
+table thead tr td {
+       font-weight: bold;
+}
+th {
+       padding: 40px;
+}
+tr.packagegroup {
+       border: 1px solid;
+}
+td {
+       vertical-align: top;
+}
+tr.packagegroup td span.name {
+}
+tr.packagegroup td a.log {
+}
+
+tr.filter {
+       background-color: lightgray;
+}
+span {
+}
+-->
+</style>
+<script>
+/*
+        @licstart  The following is the entire license notice for the
+        JavaScript code in this page.
+
+        Copyright (C) 2018  Danny Milosavljevic
+
+        The JavaScript code in this page is free software: you can
+        redistribute it and/or modify it under the terms of the GNU
+        General Public License (GNU GPL) as published by the Free Software
+        Foundation, either version 3 of the License, or (at your option)
+        any later version.  The code is distributed WITHOUT ANY WARRANTY;
+        without even the implied warranty of MERCHANTABILITY or FITNESS
+        FOR A PARTICULAR PURPOSE.  See the GNU GPL for more details.
+
+        As additional permission under GNU GPL version 3 section 7, you
+        may distribute non-source (e.g., minimized or compacted) forms of
+        that code without the copy of the GNU GPL normally required by
+        section 4, provided you include this license notice and a URL
+        through which recipients can access the Corresponding Source.
+
+        @licend  The above is the entire license notice
+        for the JavaScript code in this page.
+*/
+</script>
+<script>
+<!--
+'use strict';
+
+function td(child) {
+       let r = document.createElement("td");
+       r.appendChild(child);
+       return r;
+}
+
+function setStatusText(text) {
+       let currentdate = new Date();
+       let s = currentdate.toString() + " " + text;
+       document.getElementById("status").textContent = s;
+}
+
+let retrydelay = 3000; // ms
+function tryAgain(thunk) {
+       /* TODO random backoff */
+       window.setTimeout(thunk, retrydelay);
+       //if (retrydelay < 30000)
+       //      retrydelay = retrydelay * 2;
+}
+
+let URLPREFIX = "https://berlin.guixsd.org/";;
+//let URLPREFIX = "http://localhost:8080/";;
+let APIURLPREFIX = URLPREFIX + "api/";
+function getQueryValues(parametername) {
+       var result = [];
+       var tmp = [];
+       location.search.substr(1).split(/[&;]/).forEach(function (item) {
+               let parts = item.split("=", 2);
+               if (parts[0] === parametername)
+                       result.push(decodeURIComponent(parts[1]));
+       });
+       return result;
+}
+
+let systems = []; // will be filled by jobsetsrequest.
+let project = (getQueryValues("project").concat(["core-updates"]))[0]; // 
repo-name
+let jobset = (getQueryValues("jobset").concat(["core-updates"]))[0]; // branch
+let APIURLSUFFIX = "&project=" + encodeURIComponent(project) + "&jobset=" + 
encodeURIComponent(jobset);
+
+/* Finds the newest build per system and hides all the others in BUILDSTD */
+function hideOlderbuilds(buildstd, buildtimestd) {
+       let builds = buildstd.getElementsByTagName("div");
+       let buildtimes = buildtimestd.getElementsByTagName("span");
+       let i = 0;
+       let maximaltimes = new Object();
+       let maximalbuildids = new Object();
+       for (i = 0; i < builds.length; ++i) {
+               let build = builds[i];
+               let buildid = build.name*1;
+               let buildtimeelement = buildtimes[i + 1]; // first is the 
maximum.
+               let a = build.getElementsByTagName("a")[0];
+               let system = a.name;
+               let buildtime = buildtimeelement.textContent*1;
+               if (!maximaltimes[system] || buildtime > maximaltimes[system]) {
+                       maximaltimes[system] = buildtime;
+                       maximalbuildids[system] = 0;
+               } else if (maximaltimes[system] && buildtime == 
maximaltimes[system]) {
+                       // disambiguate.
+                       if (!maximalbuildids[system] || buildid > 
maximalbuildids[system])
+                               maximalbuildids[system] = buildid;
+               }
+       }
+       for (i = 0; i < builds.length; ++i) {
+               let build = builds[i];
+               let buildid = build.name*1;
+               let buildtimeelement = buildtimes[i + 1]; // first is the 
maximum.
+               let a = build.getElementsByTagName("a")[0];
+               let system = a.name;
+               let buildtime = buildtimeelement.textContent*1;
+               if (buildtime < maximaltimes[system] || (buildtime == 
maximaltimes[system] && (maximalbuildids[system] && buildid != 
maximalbuildids[system]))) {
+                       build.style.display = "none";
+               }
+       }
+}
+
+/** Given a TD which has build time HTML child elements, updates the first 
element to the maximum of the others.
+    Precondition: First element has no name. */
+function updateLatestbuildtime(td) {
+       let latestbuildtimenode = td.childNodes[0];
+       let value = new Date(0);
+       // Find maximum
+       Array.prototype.forEach.call(td.childNodes, function(e) {
+               if (e.name) {
+                       let xvalue = new Date(e.textContent*1);
+                       if (xvalue > value)
+                               value = xvalue;
+               }
+       });
+       latestbuildtimenode.textContent = value;
+}
+
+function getElementsByName(root, name) {
+       let childelements = root.getElementsByTagName("*"); // also returns 
itself, sigh...
+       let result = [];
+       Array.prototype.forEach.call(childelements, function(e) {
+               if (root != e && e.name == name)
+                       result.push(e);
+       });
+       return result;
+}
+
+// XXX: I think getElementsByTagName does depth recursion.  That's not what I 
want.
+
+/** Given a TABLE and PACKAGEGROUPID, removes the entry for build BUILDID from 
it. */
+function removePackagegroupelement(table, packagegroupid, buildid) {
+       let rootbody = table.getElementsByTagName("tbody")[0];
+       let packagegroups = getElementsByName(rootbody, "packagegroup_" + 
packagegroupid);
+       if (packagegroups.length > 0) {
+               let packagegroup = packagegroups[0];
+               // Note: buildsTd and buildtimesTd should have the same number 
of payload data elements.
+               let buildsTd = getElementsByName(packagegroup, "builds")[0];
+               let buildtimesTd = getElementsByName(packagegroup, 
"buildtimes")[0];
+               let logentries = getElementsByName(buildsTd, buildid);
+               let buildtimeentries = getElementsByName(buildtimesTd, buildid);
+               if (buildtimeentries.length > 0) {
+                       let buildtimeentry = buildtimeentries[0];
+                       buildtimesTd.removeChild(buildtimeentry);
+                       updateLatestbuildtime(buildtimesTd);
+               }
+               if (logentries.length > 0) {
+                       let logentry = logentries[0];
+                       buildsTd.removeChild(logentry);
+               }
+               if (buildsTd.childNodes.length == 0) { /* empty packagegroup */
+                       rootbody.removeChild(packagegroup);
+               }
+       }
+}
+
+/** Given a TD and NAME, replaces the element by VALUER().
+    If it didn't exist yet, adds it. */
+function updateMultientry(td, name, valuer) {
+       let entries = getElementsByName(td, name);
+       let span = valuer();
+       if (entries.length == 0) {
+               //if (td.childNodes.length != 0 && span.style.visibility != 
"none") {
+               //      let br = document.createElement("br");
+               //      td.appendChild(br);
+               //}
+               td.appendChild(span);
+       } else {
+               td.replaceChild(span, entries[0]);
+       }
+}
+
+function element(tagName, name) {
+       let r = document.createElement(tagName);
+       r.name = name;
+       r.className = name;
+       return r;
+}
+
+/** Given a TABLE and PACKAGEGROUPID, adds an entry for build BUILDID to it.
+LINK specifies whether to add a link to show the log. */
+function addPackagegroupelement(table, packagegroupid, buildid, system, 
buildtime, loglink) {
+       let rootbody = table.getElementsByTagName("tbody")[0];
+       let packagegroups = getElementsByName(rootbody, "packagegroup_" + 
packagegroupid);
+       if (packagegroups.length == 0) {
+               let tr = document.createElement("tr");
+               tr.name = "packagegroup_" + packagegroupid;
+               tr.className = "packagegroup";
+
+               let packagenamespan = element("span", "packagename");
+               packagenamespan.textContent = packagegroupid;
+               tr.appendChild(td(packagenamespan));
+
+               tr.appendChild(element("td", "builds", "builds"));
+
+               let buildtimes = element("td", "buildtimes", "buildtimes");
+
+               let latestbuildtime = document.createElement("span");
+               buildtimes.appendChild(latestbuildtime);
+
+               tr.appendChild(buildtimes);
+
+               rootbody.appendChild(tr);
+               packagegroups = [rootbody];
+       }
+       let packagegroup = packagegroups[0];
+       let buildstd = getElementsByName(packagegroup, "builds")[0];
+       updateMultientry(buildstd, buildid, function() {
+               let div = document.createElement("div");
+               div.name = buildid;
+               let a = element("a", system);
+               if (loglink) {
+                       a.href = URLPREFIX + "build/" + 
encodeURIComponent(buildid) + "/log/raw";
+                       a.textContent = "Log " + system;
+               } else
+                       a.textContent = system;
+               a.title = system + " (built at " + new Date((buildtime*1)*1000) 
+ ")";
+               a.target = a.href;
+               div.appendChild(a);
+               return div;
+       });
+       let buildtimestd = getElementsByName(packagegroup, "buildtimes")[0];
+       updateMultientry(buildtimestd, buildid, function() {
+               let span = document.createElement("span");
+               span.name = buildid;
+               span.style.display = 'none';
+               span.textContent = (buildtime | 0)*1000; // ms
+               return span;
+       });
+       hideOlderbuilds(buildstd, buildtimestd);
+       updateLatestbuildtime(buildtimestd);
+}
+
+function cpackagegroupid(datanode) {
+       let s = datanode.job;
+       let i = s.lastIndexOf(".");
+       // Strip off ".i686-linux" etc.
+       return s.substring(0, i);
+}
+
+/** Update filtered tables */
+function refilter(table, filters) {
+       let rootbody = table.getElementsByTagName("tbody")[0];
+       let trs = rootbody.getElementsByTagName("tr");
+       Array.prototype.forEach.call(trs, function (tr) {
+               let visible = true;
+               let tds = tr.getElementsByTagName("td");
+               let i = 0;
+               for (i = 0; i < filters.length; ++i) {
+                       let check = filters[i];
+                       if (!check(tds[i]))
+                               visible = false;
+               }
+               tr.style.display = visible ? "table-row" : "none";
+       });
+}
+
+let yesman = function(td) {
+       return true;
+};
+
+// Selected filters
+
+let latestbuildsfilters = [yesman, yesman, yesman];
+let queuedbuildsfilters = [yesman, yesman, yesman];
+
+/** Given a JSONRESPONSE, makes sure that the queued builds in there are 
displayed.
+(Note: the Log link makes little sense since it doesn't work here) */
+function displayQueuedbuilds(jsonResponse) {
+       let latestbuilds = document.getElementById("latestbuilds");
+       let queuedbuildsElement = document.getElementById("queuedbuilds");
+       jsonResponse.forEach(function (datanode) {
+               console.log(datanode);
+               let packagegroupid = cpackagegroupid(datanode);
+               let buildid = datanode.id;
+               let system = datanode.system;
+               let buildtime = datanode.timestamp;
+               removePackagegroupelement(latestbuilds, packagegroupid, 
buildid);
+               addPackagegroupelement(queuedbuildsElement, packagegroupid, 
buildid, system, buildtime, /*link*/false);
+       });
+       refilter(queuedbuilds, queuedbuildsfilters);
+}
+
+/** Given a JSONRESPONSE, makes sure that the latest builds in there are 
displayed. */
+function displayLatestbuilds(jsonResponse) {
+       let latestbuilds = document.getElementById("latestbuilds");
+       let queuedbuildsElement = document.getElementById("queuedbuilds");
+       jsonResponse.forEach(function (datanode) {
+               console.log(datanode);
+               let packagegroupid = cpackagegroupid(datanode);
+               let buildid = datanode.id;
+               let system = datanode.system;
+               let buildtime = datanode.stoptime || datanode.timestamp;
+               removePackagegroupelement(queuedbuildsElement, packagegroupid, 
buildid);
+               addPackagegroupelement(latestbuilds, packagegroupid, buildid, 
system, buildtime, /*link*/true);
+       });
+       refilter(latestbuilds, latestbuildsfilters);
+}
+
+/** Starts a JSON request on resource URL.
+    If completed successfully, continues with onload(responseJSON).
+    If completed with error, continues with onerror(status, statusText). */
+function startRequest(url, onload, onerror) {
+       let req = new XMLHttpRequest();
+       req.timeout = 10000; // ms
+       req.overrideMimeType("application/json");
+       req.onerror = function(e) {
+               onerror(req.status, req.statusText);
+       };
+       req.onload = function() {
+               if (req.status == 200) {
+                       //let response = req.responseJSON;
+                       let response = JSON.parse(req.responseText);
+                       //console.log(response);
+                       onload(response);
+               } else if (req.status == 502) { // Bad gateway.
+                       setStatusText("Bad gateway.  Trying again...");
+                       tryAgain(function() {
+                               startRequest(url, onload, onerror);
+                       });
+               } else if (req.status == 504) { // Gateway timeout.
+                       setStatusText("Gateway timeout.  Trying again...");
+                       tryAgain(function() {
+                               startRequest(url, onload, onerror);
+                       });
+               } else {
+                       setStatusText(req.status);
+               }
+       };
+       req.ontimeout = function() {
+               setStatusText("Timeout.  Trying again...");
+               tryAgain(function() {
+                       startRequest(url, onload, onerror);
+               });
+       };
+       req.open("GET", url, /*async */true);
+       //req.responseType = "json";
+       req.send(null);
+       // Access-Control-Allow-Origin: *
+}
+
+// Unused for now
+function startBuildstatusrequest(buildid) {
+       startRequest(URLPREFIX + "build/" + encodeURIComponent(buildid) + 
"?nr=1" + APIURLSUFFIX, function(response) {
+               // (same as in latestbuilds, queuedbuilds)
+               // response.id
+               // response.project
+               // response.jobset
+               // response.job
+               // response.timetamp [build creation]
+               // response.stoptime
+               // response.buildoutputs.out.path
+               // response.system
+               // response.nixname
+               // response.buildstatus [0: succeeded, 1: failed, 2: failed 
dep, 3: failed outer; 4 cancelled]
+               alert(response);
+       }, function(status, statusText) {
+       });
+}
+
+function createOption(value) {
+       let r = document.createElement("option");
+       r.value = value;
+       r.innerHTML = value;
+       return r;
+}
+
+function isOptionPresent(root, value) {
+       let options = root.getElementsByTagName("option");
+       let i = 0;
+       for (i = 0; i < options.length; ++i) {
+               let option = options[i];
+               if (option.value == value)
+                       return true;
+       }
+       return false;
+}
+
+function startBuildlogrequest(buildid) {
+       let w = window.open(URLPREFIX + "build/" + encodeURIComponent(buildid) 
+ "/log/raw?nr=1" + APIURLSUFFIX, "buildlog_" + buildid);
+       w.onerror = function() {
+               tryAgain(function() {
+                       w.reload();
+               });
+       };
+}
+
+// TODO API params: system (premature optimization)
+// TODO status: pending or done or failed(!).
+
+/** Gets a list of the latest successful builds. */
+function startLatestbuildsrequest() {
+       return startRequest(APIURLPREFIX + "latestbuilds?nr=50" + APIURLSUFFIX, 
function(response) {
+               //console.log(response);
+               displayLatestbuilds(response);
+               window.setTimeout(startLatestbuildsrequest, 50000 /* ms */);
+       }, function(status, statusText) {
+               setStatusText("Error " + status + " " + statusText + ".  Trying 
again...");
+               tryAgain(startLatestbuildsrequest);
+       });
+}
+
+/** Gets a list of the latest queued builds. */
+function startQueuerequest() {
+       return startRequest(APIURLPREFIX + "queue?nr=50" + APIURLSUFFIX, 
function(response) {
+               //console.log(response);
+               displayQueuedbuilds(response);
+               window.setTimeout(startQueuerequest, 30000 /* ms */);
+       }, function(status, statusText) {
+               setStatusText("Error " + status + " " + statusText + ".  Trying 
again...");
+               tryAgain(startQueuerequest);
+       });
+}
+
+/** Updates the filter for column INDEX to CALLBACK, then refilters. */
+function updateFilter(table, filters, index, callback) {
+       filters[index] = callback;
+       refilter(table, filters);
+}
+
+function installPackagenamefilter(table, td, filters, querypackagenames) {
+       let inputs = td.getElementsByTagName("input");
+       Array.prototype.forEach.call(inputs, function(e) {
+               if (querypackagenames.length > 0) {
+                       e.value = querypackagenames[0];
+               }
+               e.oninput = function() {
+                       updateFilter(table, filters, 0, function(td) {
+                               let packagenamespan = getElementsByName(td, 
"packagename")[0];
+                               return 
packagenamespan.textContent.startsWith(e.value);
+                       });
+               };
+       });
+}
+
+function checkbox(name) {
+       let r = document.createElement("input");
+       r.type = "checkbox";
+       r.name = name;
+       r.value = name;
+       r.checked = true;
+       return r;
+}
+
+function installBuildfilter(table, td, filters, querybuilds) {
+       let values = systems;
+       let widgets = values.map(function(value) {
+               let input = element("input", value);
+               input.type = "checkbox";
+               input.name = value;
+               input.title = value; // .replace(/-linux$/, "");
+               input.checked = querybuilds.length == 0 || 
querybuilds.indexOf(value) != -1;
+               td.appendChild(input);
+               return input;
+       });
+       function check(td) {
+               let divs = td.getElementsByTagName("div");
+               let j = 0;
+               for (j = 0; j < divs.length; ++j) {
+                       let div = divs[j];
+                       let a = div.getElementsByTagName("a")[0];
+                       let i = 0;
+                       for (i = 0; i < widgets.length; ++i) {
+                               let widget = widgets[i];
+                               if (widget.checked) {
+                                       let xsystem = a.title.split(" ")[0];
+                                       if (xsystem == widget.name)
+                                               return true;
+                               }
+                       }
+               }
+               return false;
+       }
+       function check1() {
+               return updateFilter(table, filters, 1, check);
+       }
+       widgets.forEach(function(widget) {
+               widget.oninput = check1;
+       });
+}
+
+function installBuildfilter2(tableid, filters) {
+       let table = document.getElementById(tableid);
+       let thead = table.getElementsByTagName("thead")[0];
+       let theadtr = thead.getElementsByTagName("tr")[0];
+       let headertds = theadtr.getElementsByTagName("td");
+       let headerbuildstd = headertds[1];
+       installBuildfilter(table, headerbuildstd, filters, 
getQueryValues("systems"));
+}
+
+function startJobsetsrequest() {
+       startRequest(URLPREFIX + "jobsets?nr=100", function(response) {
+               let projectelement = document.getElementById("project");
+               let jobsetelement = document.getElementById("jobset");
+               systems = [];
+
+               let row = response; // pretty sure cuirass has a bug here.
+               let project = row.name;
+               let jobset = row.branch;
+               let xsystems = row.arguments.systems;
+
+               systems = systems.concat(xsystems.filter(function (item) {
+                       return systems.indexOf(item) == -1;
+               }));
+               if (!isOptionPresent(projectelement, project))
+                       projectelement.appendChild(createOption(project));
+               if (!isOptionPresent(jobsetelement, jobset))
+                       jobsetelement.appendChild(createOption(jobset));
+
+               // The global systems are now correct, so install these filters 
only now.
+               installBuildfilter2('latestbuilds', latestbuildsfilters);
+               installBuildfilter2('queuedbuilds', queuedbuildsfilters);
+       }, function(status, statusText) {
+       });
+}
+
+function installBuildtimefilter(table, td, filters, querybuildtimes) {
+       let inputs = td.getElementsByTagName("input");
+       Array.prototype.forEach.call(inputs, function(e) {
+               if (querybuildtimes.length > 0)
+                       e.value = querybuildtimes[0];
+               e.oninput = function() {
+                       updateFilter(table, filters, 2, function(td) {
+                               if (!e.value)
+                                       return true;
+                               let value = new Date(e.value);
+                               let span = td.getElementsByTagName("span")[0];
+                               let xvalue = new Date(span.textContent);
+                               if (xvalue >= value)
+                                       return true;
+                               return false;
+                       });
+               };
+       });
+}
+
+function installFilters(tableid, filters) {
+       let table = document.getElementById(tableid);
+       let thead = table.getElementsByTagName("thead")[0];
+       let theadtr = thead.getElementsByTagName("tr")[0];
+       let headertds = theadtr.getElementsByTagName("td");
+       let headerpackagenametd = headertds[0];
+       let headerbuildstd = headertds[1];
+       let headerbuildtimestd = headertds[2];
+       installPackagenamefilter(table, headerpackagenametd, filters, 
getQueryValues("packagename"));
+       // done when we have jobsets. installBuildfilter(table, headerbuildstd, 
filters, getQueryValues("systems"));
+       installBuildtimefilter(table, headerbuildtimestd, filters, 
getQueryValues("buildtime"));
+       refilter(table, filters);
+}
+
+/** Call only once! */
+function updateBranchinfo() {
+       let projectelement = document.getElementById("project");
+       let jobsetelement = document.getElementById("jobset");
+       function switchBranch() {
+               let search = "project=" + 
encodeURIComponent(projectelement.value) + "&jobset=" + 
encodeURIComponent(jobsetelement.value) + "&" + location.search.substr(1);
+               // TODO Recover anchors?
+               location.href = location.href.split("?")[0] + "?" + search;
+       }
+       let projectoptions = [];
+       // The user is always right.
+       if (projectoptions.indexOf(project) == -1)
+               projectoptions.push(project);
+       projectoptions.forEach(function(value) {
+               projectelement.appendChild(createOption(value));
+       });
+       projectelement.value = project;
+       projectelement.onchange = switchBranch;
+       let jobsetoptions = [];
+       // The user is always right.
+       if (jobsetoptions.indexOf(jobset) == -1)
+               jobsetoptions.push(jobset);
+       jobsetoptions.forEach(function(value) {
+               jobsetelement.appendChild(createOption(value));
+       });
+       jobsetelement.value = jobset;
+       jobsetelement.onchange = switchBranch;
+       startJobsetsrequest();
+}
+
+//-->
+</script>
+</head>
+<body onload="updateBranchinfo(); installFilters('latestbuilds', 
latestbuildsfilters); installFilters('queuedbuilds', queuedbuildsfilters); 
startLatestbuildsrequest(); startQueuerequest();">
+<form>
+<div class="branch">Project: <select id="project" name="project"></select>; 
jobset: <select name="jobset" id="jobset"></select></div>
+</form>
+<div class="status">Request status:
+  <span id="status">
+  </span>
+</div>
+<!-- TODO sort alphabetically or by time -->
+<h1>Finished Builds</h1>
+<form>
+<table class="latestbuilds" id="latestbuilds">
+<thead><tr class="filter"><td>Starts with <input type="text" 
name="packagenamefilter"></td><td>At least </td><td>At least <input 
type="datetime-local" name="buildtimefilter"></td></tr>
+<tr><td>Packagename</td><td>Builds</td><td>Latest build finished 
at</td></tr></thead>
+<tbody>
+</tbody>
+</table>
+</form>
+<h1>Queued Builds</h1>
+<form>
+<table class="queuedbuilds" id="queuedbuilds">
+<thead><tr class="filter"><td>Starts with <input type="text" 
name="packagenamefilter"></td><td>At least </td><td>At least <input 
type="datetime-local" name="buildtimefilter"></td></tr>
+<tr><td>Packagename</td><td>Builds</td><td>Build most recently enqueued 
at</td></tr></thead>
+<tbody>
+</tbody>
+</table>
+</form>
+<!--
+<h1>Failed Builds</h1>
+<form>
+<table class="failedbuilds" id="failedbuilds">
+<thead><tr class="filter"><td>Starts with <input type="text" 
name="packagenamefilter"></td><td>At least </td><td>At least <input 
type="datetime-local" name="buildtimefilter"></td></tr>
+<tr><td>Packagename</td><td>Builds</td><td>Build most recently enqueued 
at</td></tr></thead>
+<tbody>
+</tbody>
+</table>
+</form>
+-->
+
+</body>
+</html>



reply via email to

[Prev in Thread] Current Thread [Next in Thread]