aboutsummaryrefslogtreecommitdiff
path: root/assets
diff options
context:
space:
mode:
authorChristopher Baines <mail@cbaines.net>2023-04-13 09:56:34 +0100
committerChristopher Baines <mail@cbaines.net>2023-04-13 16:41:30 +0100
commit27c8de16b55dfe70180f0e17e12f442696708a53 (patch)
tree0c9260025f28b6948629e79762183b480fe52254 /assets
downloadbffe-27c8de16b55dfe70180f0e17e12f442696708a53.tar
bffe-27c8de16b55dfe70180f0e17e12f442696708a53.tar.gz
Initial commit
Diffstat (limited to 'assets')
-rw-r--r--assets/css/mvp.css482
-rw-r--r--assets/js/activity.js351
2 files changed, 833 insertions, 0 deletions
diff --git a/assets/css/mvp.css b/assets/css/mvp.css
new file mode 100644
index 0000000..6402648
--- /dev/null
+++ b/assets/css/mvp.css
@@ -0,0 +1,482 @@
+/* MVP.css v1.8 - https://github.com/andybrewer/mvp */
+
+:root {
+ --active-brightness: 0.85;
+ --border-radius: 5px;
+ --box-shadow: 2px 2px 10px;
+ --color: #118bee;
+ --color-accent: #118bee15;
+ --color-bg: #fff;
+ --color-bg-secondary: #e9e9e9;
+ --color-link: #118bee;
+ --color-secondary: #920de9;
+ --color-secondary-accent: #920de90b;
+ --color-shadow: #f4f4f4;
+ --color-table: #118bee;
+ --color-text: #000;
+ --color-text-secondary: #999;
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
+ --hover-brightness: 1.2;
+ --justify-important: center;
+ --justify-normal: left;
+ --line-height: 1.5;
+ --width-card: 285px;
+ --width-card-medium: 460px;
+ --width-card-wide: 800px;
+ --width-content: 1080px;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --color: #0097fc;
+ --color-accent: #0097fc4f;
+ --color-bg: #333;
+ --color-bg-secondary: #555;
+ --color-link: #0097fc;
+ --color-secondary: #e20de9;
+ --color-secondary-accent: #e20de94f;
+ --color-shadow: #bbbbbb20;
+ --color-table: #0097fc;
+ --color-text: #f7f7f7;
+ --color-text-secondary: #aaa;
+ }
+}
+
+/* Layout */
+article aside {
+ background: var(--color-secondary-accent);
+ border-left: 4px solid var(--color-secondary);
+ padding: 0.01rem 0.8rem;
+}
+
+body {
+ background: var(--color-bg);
+ color: var(--color-text);
+ font-family: var(--font-family);
+ line-height: var(--line-height);
+ margin: 0;
+ overflow-x: hidden;
+ padding: 0;
+}
+
+footer,
+header,
+main {
+ margin: 0 auto;
+ max-width: var(--width-content);
+ padding: 3rem 1rem;
+}
+
+hr {
+ background-color: var(--color-bg-secondary);
+ border: none;
+ height: 1px;
+ margin: 4rem 0;
+ width: 100%;
+}
+
+section {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: var(--justify-important);
+}
+
+section img,
+article img {
+ max-width: 100%;
+}
+
+section pre {
+ overflow: auto;
+}
+
+section aside {
+ border: 1px solid var(--color-bg-secondary);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow) var(--color-shadow);
+ margin: 1rem;
+ padding: 1.25rem;
+ width: var(--width-card);
+}
+
+section aside:hover {
+ box-shadow: var(--box-shadow) var(--color-bg-secondary);
+}
+
+[hidden] {
+ display: none;
+}
+
+/* Headers */
+article header,
+div header,
+main header {
+ padding-top: 0;
+}
+
+header {
+ text-align: var(--justify-important);
+}
+
+header a b,
+header a em,
+header a i,
+header a strong {
+ margin-left: 0.5rem;
+ margin-right: 0.5rem;
+}
+
+header nav img {
+ margin: 1rem 0;
+}
+
+section header {
+ padding-top: 0;
+ width: 100%;
+}
+
+/* Nav */
+nav {
+ align-items: center;
+ display: flex;
+ font-weight: bold;
+ justify-content: space-between;
+ margin-bottom: 7rem;
+}
+
+nav ul {
+ list-style: none;
+ padding: 0;
+}
+
+nav ul li {
+ display: inline-block;
+ margin: 0 0.5rem;
+ position: relative;
+ text-align: left;
+}
+
+/* Nav Dropdown */
+nav ul li:hover ul {
+ display: block;
+}
+
+nav ul li ul {
+ background: var(--color-bg);
+ border: 1px solid var(--color-bg-secondary);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow) var(--color-shadow);
+ display: none;
+ height: auto;
+ left: -2px;
+ padding: .5rem 1rem;
+ position: absolute;
+ top: 1.7rem;
+ white-space: nowrap;
+ width: auto;
+ z-index: 1;
+}
+
+nav ul li ul::before {
+ /* fill gap above to make mousing over them easier */
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: -0.5rem;
+ height: 0.5rem;
+}
+
+nav ul li ul li,
+nav ul li ul li a {
+ display: block;
+}
+
+/* Typography */
+code,
+samp {
+ background-color: var(--color-accent);
+ border-radius: var(--border-radius);
+ color: var(--color-text);
+ display: inline-block;
+ margin: 0 0.1rem;
+ padding: 0 0.5rem;
+}
+
+details {
+ margin: 1.3rem 0;
+}
+
+details summary {
+ font-weight: bold;
+ cursor: pointer;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ line-height: var(--line-height);
+}
+
+mark {
+ padding: 0.1rem;
+}
+
+ol li,
+ul li {
+ padding: 0.2rem 0;
+}
+
+p {
+ margin: 0.75rem 0;
+ padding: 0;
+ width: 100%;
+}
+
+pre {
+ margin: 1rem 0;
+ max-width: var(--width-card-wide);
+ padding: 1rem 0;
+}
+
+pre code,
+pre samp {
+ display: block;
+ max-width: var(--width-card-wide);
+ padding: 0.5rem 2rem;
+ white-space: pre-wrap;
+}
+
+small {
+ color: var(--color-text-secondary);
+}
+
+sup {
+ background-color: var(--color-secondary);
+ border-radius: var(--border-radius);
+ color: var(--color-bg);
+ font-size: xx-small;
+ font-weight: bold;
+ margin: 0.2rem;
+ padding: 0.2rem 0.3rem;
+ position: relative;
+ top: -2px;
+}
+
+/* Links */
+a {
+ color: var(--color-link);
+ display: inline-block;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+a:active {
+ filter: brightness(var(--active-brightness));
+ text-decoration: underline;
+}
+
+a:hover {
+ filter: brightness(var(--hover-brightness));
+ text-decoration: underline;
+}
+
+a b,
+a em,
+a i,
+a strong,
+button {
+ border-radius: var(--border-radius);
+ display: inline-block;
+ font-size: medium;
+ font-weight: bold;
+ line-height: var(--line-height);
+ margin: 0.5rem 0;
+ padding: 1rem 2rem;
+}
+
+button {
+ font-family: var(--font-family);
+}
+
+button:active {
+ filter: brightness(var(--active-brightness));
+}
+
+button:hover {
+ cursor: pointer;
+ filter: brightness(var(--hover-brightness));
+}
+
+a b,
+a strong,
+button {
+ background-color: var(--color-link);
+ border: 2px solid var(--color-link);
+ color: var(--color-bg);
+}
+
+a em,
+a i {
+ border: 2px solid var(--color-link);
+ border-radius: var(--border-radius);
+ color: var(--color-link);
+ display: inline-block;
+ padding: 1rem 2rem;
+}
+
+article aside a {
+ color: var(--color-secondary);
+}
+
+/* Images */
+figure {
+ margin: 0;
+ padding: 0;
+}
+
+figure img {
+ max-width: 100%;
+}
+
+figure figcaption {
+ color: var(--color-text-secondary);
+}
+
+/* Forms */
+
+button:disabled,
+input:disabled {
+ background: var(--color-bg-secondary);
+ border-color: var(--color-bg-secondary);
+ color: var(--color-text-secondary);
+ cursor: not-allowed;
+}
+
+button[disabled]:hover {
+ filter: none;
+}
+
+form {
+ border: 1px solid var(--color-bg-secondary);
+ border-radius: var(--border-radius);
+ box-shadow: var(--box-shadow) var(--color-shadow);
+ display: block;
+ max-width: var(--width-card-wide);
+ min-width: var(--width-card);
+ padding: 1.5rem;
+ text-align: var(--justify-normal);
+}
+
+form header {
+ margin: 1.5rem 0;
+ padding: 1.5rem 0;
+}
+
+input,
+label,
+select,
+textarea {
+ display: block;
+ font-size: inherit;
+ max-width: var(--width-card-wide);
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+ display: inline-block;
+}
+
+input[type="checkbox"]+label,
+input[type="radio"]+label {
+ display: inline-block;
+ font-weight: normal;
+ position: relative;
+ top: 1px;
+}
+
+input,
+select,
+textarea {
+ border: 1px solid var(--color-bg-secondary);
+ border-radius: var(--border-radius);
+ margin-bottom: 1rem;
+ padding: 0.4rem 0.8rem;
+}
+
+input[readonly],
+textarea[readonly] {
+ background-color: var(--color-bg-secondary);
+}
+
+label {
+ font-weight: bold;
+ margin-bottom: 0.2rem;
+}
+
+/* Tables */
+table {
+ border: 1px solid var(--color-bg-secondary);
+ border-radius: var(--border-radius);
+ border-spacing: 0;
+ display: inline-block;
+ max-width: 100%;
+ overflow-x: auto;
+ padding: 0;
+ white-space: nowrap;
+}
+
+table td,
+table th,
+table tr {
+ padding: 0.4rem 0.8rem;
+ text-align: var(--justify-important);
+}
+
+table thead {
+ background-color: var(--color-table);
+ border-collapse: collapse;
+ border-radius: var(--border-radius);
+ color: var(--color-bg);
+ margin: 0;
+ padding: 0;
+}
+
+table thead th:first-child {
+ border-top-left-radius: var(--border-radius);
+}
+
+table thead th:last-child {
+ border-top-right-radius: var(--border-radius);
+}
+
+table thead th:first-child,
+table tr td:first-child {
+ text-align: var(--justify-normal);
+}
+
+table tr:nth-child(even) {
+ background-color: var(--color-accent);
+}
+
+/* Quotes */
+blockquote {
+ display: block;
+ font-size: x-large;
+ line-height: var(--line-height);
+ margin: 1rem auto;
+ max-width: var(--width-card-medium);
+ padding: 1.5rem 1rem;
+ text-align: var(--justify-important);
+}
+
+blockquote footer {
+ color: var(--color-text-secondary);
+ display: block;
+ font-size: small;
+ line-height: var(--line-height);
+ padding: 1.5rem 0;
+}
diff --git a/assets/js/activity.js b/assets/js/activity.js
new file mode 100644
index 0000000..719ec47
--- /dev/null
+++ b/assets/js/activity.js
@@ -0,0 +1,351 @@
+
+const recentActivityDiv = document.getElementById("recent-activity-body");
+const recentActivityDivLimit = 1000;
+
+function addRecentActivityEntry(event, content, timestamp) {
+ const eventRow = document.createElement('tr');
+ eventRow.className = "event";
+ eventRow.innerHTML = `
+<td width="10%">${event}</td>
+<td>${content}</td>
+<td width="10%">${timestamp}</td>`;
+
+ recentActivityDiv.prepend(eventRow);
+ animateElementAddition(eventRow);
+
+ const childCount = recentActivityDiv.childElementCount;
+
+ if (childCount > recentActivityDivLimit) {
+ for (let index = recentActivityDivLimit; index < childCount; index++) {
+ recentActivityDiv.removeChild(
+ recentActivityDiv.children[index]
+ )
+ }
+ }
+}
+
+function animateElementAddition(e, color = "lightblue") {
+ e.style.backgroundColor = color;
+ e.style.lineHeight = "0px";
+ e.style.overflow = "hidden";
+ e.style.opacity = 0;
+
+ setTimeout(function() {
+ e.style.transition =
+ "background-color 1.2s ease-out, line-height 6s, opacity 0.3s ease-out";
+ e.style.backgroundColor = "";
+ e.style.lineHeight = "";
+ e.style.opacity = "";
+ }, 50);
+}
+
+function animateElementChange(e, color = "lightblue") {
+ e.style.transition = "background-color 0.6s ease-out";
+
+ setTimeout(function() {
+ e.style.backgroundColor = color;
+
+ setTimeout(function() {
+ e.style.backgroundColor = "";
+ }, 600);
+ }, 50);
+}
+
+function animateElementAndRemove(e, color) {
+ // TODO: This needs more work, as it needs to fit in with keeping the
+ // related bits that reference the number of builds up to date
+ e.remove();
+
+ // e.style.borderWidth = "20px;";
+ // e.style.borderStyle = "solid";
+ // e.style.borderColor = color;
+ // e.style.transition = "border 10s";
+ // e.addEventListener("transitionend", function() {
+ // e.remove();
+ // }, {once: true});
+
+ // setTimeout(function() {
+ // e.style.borderColor = "";
+ // }, 1000);
+}
+
+function buildDetailsFromDom(id) {
+ return document.getElementById("build-" + id).dataset;
+}
+
+function agentDetailsFromDom(id) {
+ return document.getElementById("agent-" + id).dataset;
+}
+
+function updateAgentBuildRelatedElements(agentId) {
+ const agentBuildsElement = document.getElementById(
+ "agent-" + agentId + "-builds"
+ );
+ const childCount = agentBuildsElement.childElementCount;
+
+ const noAllocatedBuildsElement = document.getElementById(
+ "agent-" + agentId + "-no-allocated-builds"
+ );
+ const plusXBuildsElement = document.getElementById(
+ "agent-" + agentId + "-plus-x-builds"
+ );
+
+
+ if (childCount == 0) {
+ noAllocatedBuildsElement.style.display = "block";
+ plusXBuildsElement.style.display = "none";
+
+ animateElementChange(noAllocatedBuildsElement);
+ } else {
+ for (let index = 0; index < Math.min(childCount, 4); index++) {
+ agentBuildsElement.children[index].style.display = "block";
+ }
+
+ if (childCount > 4) {
+ noAllocatedBuildsElement.style.display = "none";
+
+ plusXBuildsElement.style.display = "block";
+
+ const otherBuildCount = childCount - 4;
+ if (otherBuildCount == 1) {
+ plusXBuildsElement.textContent = "Plus 1 other build";
+ } else {
+ plusXBuildsElement.textContent =
+ `Plus ${otherBuildCount} other builds`;
+ }
+
+ animateElementChange(plusXBuildsElement);
+ } else {
+ noAllocatedBuildsElement.style.display = "none";
+ plusXBuildsElement.style.display = "none";
+ }
+ }
+}
+
+function buildSubmittedHandler(e) {
+ const data = JSON.parse(e.data);
+ console.log("build-submitted", data);
+
+ addRecentActivityEntry(
+ "Build submitted",
+ `${data.derivation.slice(44, -4)}`,
+ data.timestamp
+ );
+}
+
+function buildCanceledHandler(e) {
+ console.log("build-canceled", e);
+
+ addRecentActivityEntry(
+ "Build canceled",
+ `${data.derivation.slice(44, -4)}, tags: ${data.tags}`,
+ data.timestamp
+ );
+}
+
+function buildSuccessHandler(e) {
+ const data = JSON.parse(e.data);
+ console.log("build-success", data);
+
+ const buildElement = document.getElementById(
+ "build-" + data.build_id
+ );
+ const buildDetails = buildDetailsFromDom(data.build_id);
+
+ addRecentActivityEntry(
+ "Build success",
+ `${buildDetails.derivation.slice(44, -4)}`,
+ data.timestamp
+ );
+
+ animateElementAndRemove(buildElement, "green");
+
+ updateAgentBuildRelatedElements(data.agent_id);
+}
+
+function buildFailureHandler(e) {
+ const data = JSON.parse(e.data);
+
+ console.log("build-failure", data);
+
+ const buildElement = document.getElementById(
+ "build-" + data.build_id
+ );
+
+ animateElementAndRemove(buildElement, "red");
+
+ updateAgentBuildRelatedElements(data.agent_id);
+}
+
+function buildStartedHandler(e) {
+ const data = JSON.parse(e.data);
+ console.log("build-started", data);
+
+ const buildDetails = buildDetailsFromDom(data.build_id);
+ const agentDetails = agentDetailsFromDom(data.agent_id);
+
+ addRecentActivityEntry(
+ "Build started",
+ `${buildDetails.derivation.slice(44, -4)}, agent: ${agentDetails.name}`,
+ data.timestamp
+ );
+}
+
+function buildSetupFailureHandler(e) {
+ const data = JSON.parse(e.data);
+ console.log("build-setup-failure", data);
+
+ const buildDetails = buildDetailsFromDom(data.build_id);
+ const agentDetails = agentDetailsFromDom(data.agent_id);
+
+ addRecentActivityEntry(
+ "Build setup failure",
+ `${buildDetails.derivation.slice(44, -4)}, agent: ${agentDetails.name}`,
+ data.timestamp
+ );
+
+ // Remove from agent with setup failure animation
+}
+
+function allocationPlanUpdateHandler(e) {
+ const data = JSON.parse(e.data);
+
+ console.log("allocation-plan-update", data);
+
+ for (const agentId in data.allocation_plan_counts) {
+ const planSizeElement = document.getElementById(
+ "agent-" + agentId + "-plan-size"
+ );
+
+ const updatedValue = data.allocation_plan_counts[agentId];
+ if (updatedValue != planSizeElement.dataset.value) {
+ planSizeElement.textContent = `Plan size: ${data.allocation_plan_counts[agentId]}`
+ planSizeElement.dataset.value = updatedValue;
+
+ animateElementChange(planSizeElement);
+ }
+ }
+}
+
+function agentBuildsAllocatedHandler(e) {
+ const data = JSON.parse(e.data);
+
+ console.log("agent-builds-allocated", data);
+
+ const agentBuildsElement = document.getElementById(
+ "agent-" + data.agent_id + "-builds"
+ );
+
+ var newElements = [];
+
+ data.builds.forEach(function(build) {
+ const buildElement = document.getElementById("build-" + build.uuid);
+
+ if (!buildElement) {
+ const buildElement = document.createElement('div');
+ buildElement.id = "build-" + build.uuid;
+ buildElement.className = "build";
+
+ buildElement.dataset.derivation = build.derivation_name;
+ const drv = build.derivation_name.slice(44, -4);
+
+ buildElement.innerHTML = `
+<span class="monospace" style="display:block;">${drv}</span>
+`;
+
+ build.tags.sort(function(a, b) {
+ return a.key > b.key;
+ }).forEach(function(tag) {
+ var val;
+
+ if (tag.value.length == 40) {
+ val = tag.value.substring(0, 8);
+ } else {
+ val = tag.value;
+ }
+
+ buildElement.innerHTML += `
+<span class="build-tag">${tag.key}: ${val}</span>
+`;
+ });
+
+ if (agentBuildsElement.childElementCount > 4) {
+ buildElement.style.display = "none";
+ }
+
+ agentBuildsElement.append(buildElement);
+
+ newElements.push(buildElement);
+ }
+ });
+
+ if (agentBuildsElement.childElementCount > 4) {
+ document.getElementById(
+ "agent-" + data.agent_id + "-no-allocated-builds"
+ ).style.display = "none";
+
+ const plusBuilds = document.getElementById(
+ "agent-" + data.agent_id + "-plus-x-builds"
+ )
+ plusBuilds.style.display = "block";
+
+ const otherBuildCount = agentBuildsElement.childElementCount - 4;
+ if (otherBuildCount == 1) {
+ plusBuilds.textContent = "Plus 1 other build";
+ } else {
+ plusBuilds.textContent =
+ `Plus ${otherBuildCount} other builds`;
+ }
+
+ animateElementChange(plusBuilds);
+ }
+
+ for (const element of newElements) {
+ setTimeout(
+ function() { animateElementChange(element); },
+ 50
+ );
+ }
+}
+
+function agentStatusUpdateHandler(e) {
+ const data = JSON.parse(e.data);
+
+ console.log("agent-status-update", data);
+
+ const loadElement = document.getElementById(
+ "agent-" + data.agent_id + "-load"
+ );
+
+ const loadPercentage = Math.round((100 * data.load_average["1"]) / data.processor_count);
+ loadElement.textContent = `Load: ${loadPercentage}%`;
+
+ var loadClass;
+ if (loadPercentage < 150) {
+ loadClass = "agent-load-normal";
+ } else if (loadPercentage < 250) {
+ loadClass = "agent-load-medium";
+ } else {
+ loadClass = "agent-load-high";
+ }
+ loadElement.className = `agent-load ${loadClass}`;
+
+ animateElementChange(loadElement);
+}
+
+const lastEventId = document.getElementById("main").dataset.stateid;
+const evtSource = new EventSource(`/events?last_event_id=${lastEventId}`);
+
+evtSource.addEventListener("build-submitted", buildSubmittedHandler);
+evtSource.addEventListener("build-success", buildSuccessHandler);
+evtSource.addEventListener("build-failure", buildFailureHandler);
+evtSource.addEventListener("build-started", buildStartedHandler);
+evtSource.addEventListener("build-setup-failure", buildSetupFailureHandler);
+evtSource.addEventListener("allocation-plan-update", allocationPlanUpdateHandler);
+evtSource.addEventListener("agent-builds-allocated", agentBuildsAllocatedHandler);
+evtSource.addEventListener("agent-status-update", agentStatusUpdateHandler);
+
+
+console.log("starting listening");
+
+