From 27c8de16b55dfe70180f0e17e12f442696708a53 Mon Sep 17 00:00:00 2001 From: Christopher Baines Date: Thu, 13 Apr 2023 09:56:34 +0100 Subject: Initial commit --- assets/css/mvp.css | 482 ++++++++++++++++++++++++++++++++++++++++++++++++++ assets/js/activity.js | 351 ++++++++++++++++++++++++++++++++++++ 2 files changed, 833 insertions(+) create mode 100644 assets/css/mvp.css create mode 100644 assets/js/activity.js (limited to 'assets') 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 = ` +${event} +${content} +${timestamp}`; + + 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 = ` +${drv} +`; + + 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 += ` +${tag.key}: ${val} +`; + }); + + 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"); + + -- cgit v1.2.3