UNPKG

profoundjs

Version:

Profound.js Framework and Server

790 lines (711 loc) 29.3 kB
/* eslint-disable no-var */ /* eslint-disable no-undef */ "use strict"; // Description: Fetch and display a list of batch groups. // When a batch group is clicked, its details are displayed in the right column. // Querystring parameters: // - batchid: The ID of the batch group to select and display details for. If not provided, // the first batch group is selected. document.addEventListener("DOMContentLoaded", () => { if (!sessionStorage["atrium-token"]) { // Redirect to login page if not authenticated, preserving current URL as a 'redirect' parameter const currentUrl = window.location.pathname + window.location.search; window.location.href = "/auto-testing/login?redirect=" + encodeURIComponent(currentUrl); return; } // Grab all the elements we need const leftColumn = document.getElementById("left-column"); // Set default width to 400px leftColumn.style.flex = "0 0 400px"; const pageTitle = document.getElementById("pageTitle"); const itemDetails = document.getElementById("item-details"); const refreshButton = document.getElementById("refresh-button"); const resizer = document.getElementById("resizer"); let isResizing = false; // Disable text selection globally during drag document.addEventListener("selectstart", (e) => { if (isResizing) e.preventDefault(); }); // Resizer functionality resizer.addEventListener("mousedown", (e) => { isResizing = true; document.body.style.cursor = "ew-resize"; document.addEventListener("mousemove", resizeColumns); document.addEventListener("mouseup", stopResizing); }); function resizeColumns(e) { if (!isResizing) return; const containerRect = document.getElementById("resizable-container").getBoundingClientRect(); const leftWidth = e.clientX - containerRect.left; const rightWidth = containerRect.right - e.clientX; // Prevent columns from shrinking too much if (leftWidth > 200 && rightWidth > 200) { leftColumn.style.flex = `0 0 ${leftWidth}px`; // Make the right column automatically fill the remaining space itemDetails.style.flex = "1 1 auto"; } } function stopResizing() { isResizing = false; document.body.style.cursor = ""; document.removeEventListener("mousemove", resizeColumns); document.removeEventListener("mouseup", stopResizing); } // Show error message to the user in the details pane function displayErrorMessage(message) { itemDetails.innerHTML = `<div class="details error"> <p>${message}</p> </div>`; } // Environment dropdown state let envList = []; let envLookup = {}; let currentEnv = null; // Batch records cache - updated with fresh data when batch details are loaded let batchRecords = []; function renderEnvDropdown() { const filterPanel = document.getElementById("filter-panel"); if (!filterPanel || !filterPanel.parentNode) { return; } let container = document.getElementById("env-dropdown-container"); let dropdown = document.getElementById("env-dropdown"); if (!container) { container = document.createElement("div"); container.id = "env-dropdown-container"; container.style.display = "flex"; container.style.alignItems = "center"; container.style.marginBottom = "10px"; const label = document.createElement("label"); label.textContent = "Environment"; label.htmlFor = "env-dropdown"; label.id = "env-dropdown-label"; label.style.marginRight = "8px"; label.style.marginBottom = "0"; dropdown = document.createElement("select"); dropdown.id = "env-dropdown"; dropdown.style.display = "inline-block"; dropdown.style.width = "auto"; dropdown.style.flex = "none"; dropdown.addEventListener("change", () => { currentEnv = dropdown.value || null; fetchBatchIds(); }); container.appendChild(label); container.appendChild(dropdown); filterPanel.parentNode.insertBefore(container, filterPanel); } dropdown = dropdown || document.getElementById("env-dropdown"); dropdown.innerHTML = ""; if (!envList.length) { const option = document.createElement("option"); option.value = ""; option.textContent = "No environments found"; dropdown.appendChild(option); dropdown.disabled = true; currentEnv = null; return; } dropdown.disabled = false; envList.forEach(env => { const option = document.createElement("option"); option.value = env.envname; option.textContent = env.envdesc || env.envname; dropdown.appendChild(option); }); const defaultValue = currentEnv && envLookup[currentEnv] ? currentEnv : (envList[0] && envList[0].envname); dropdown.value = defaultValue || ""; currentEnv = dropdown.value || null; } function getSelectedEnv() { const dropdown = document.getElementById("env-dropdown"); if (dropdown && !dropdown.disabled && dropdown.value) { return dropdown.value; } if (currentEnv) { return currentEnv; } if (envList[0]) { return envList[0].envname; } return null; } // Fetch environment list and set up dropdown function fetchEnvList(callback) { const headers = { "Content-Type": "application/json" }; if (sessionStorage["atrium-token"]) { headers["Authorization"] = "Basic " + sessionStorage["atrium-token"]; } fetch("/auto-testing/env-list", { method: "POST", headers }) .then(res => res.json()) .then(data => { envList = Array.isArray(data.data) ? data.data : []; envLookup = {}; envList.forEach(env => { envLookup[env.envname] = env.envdesc; }); currentEnv = data.currentEnv && envLookup[data.currentEnv] ? data.currentEnv : (envList[0] && envList[0].envname) || null; renderEnvDropdown(); if (callback) callback(); }) .catch(err => { console.error("Error fetching environment list:", err); envList = []; envLookup = {}; currentEnv = null; renderEnvDropdown(); if (callback) callback(); }); } // Get batch group info from server function fetchBatchIds() { // Insert a loading spinner before fetching data const itemList = document.getElementById("item-list"); itemList.innerHTML = `<div class="spinner"></div>`; const headers = { "Content-Type": "application/json" }; if (sessionStorage["atrium-token"]) { headers["Authorization"] = "Basic " + sessionStorage["atrium-token"]; } const payload = {}; const selectedEnv = getSelectedEnv(); if (selectedEnv) { payload.env = selectedEnv; } fetch("/auto-testing/batch-list", { method: "POST", headers, body: JSON.stringify(payload) }) .then(res => res.json()) .then(data => { renderBatchIds(data.data || []); }) .catch(err => { console.error("Error fetching batch groups:", err); displayErrorMessage("Unable to reach the server. Please check your connection."); }); } function renderBatchIds(batchIds) { // Store batch records for updating with fresh data later batchRecords = batchIds; const filterPanel = document.getElementById("filter-panel"); const checkedStatuses = new Set( Array.from(filterPanel.querySelectorAll("input[type=checkbox]:checked")) .map(checkbox => checkbox.value.toLowerCase()) ); const runSummaryPanel = document.getElementById("run-summary-panel"); const checkedRunSummaries = runSummaryPanel ? new Set(Array.from(runSummaryPanel.querySelectorAll("input[type=checkbox]:checked")) .map(cb => cb.value)) : new Set(); // Helper: for a given runSummary, does it match the checked run summary filters? function runSummaryMatches(runsummary) { const val = (runsummary || "").toLowerCase(); // Always include batches that are running or not started yet. if (val === "" || val === "running" || val === "not started") { return true; } if (!checkedRunSummaries.size) { return true; } let match = false; if (checkedRunSummaries.has("passed")) { if (val === "passed" || val === "mixed") match = true; } if (checkedRunSummaries.has("failed")) { if (val === "failed" || val === "mixed") match = true; } if (checkedRunSummaries.has("warning")) { if (val === "passed with warnings" || val === "mixed with warnings") match = true; } return match; } const filteredBatches = batchIds.filter(record => { const statusKey = (record.batchstatussummary || record.abstatus || "").toLowerCase(); if (statusKey && !checkedStatuses.has(statusKey)) { return false; } return runSummaryMatches(record.runsummary); }); const itemList = document.getElementById("item-list"); itemList.innerHTML = ""; if (filteredBatches.length === 0) { itemList.innerHTML = `<div class="empty-state">No batches to display.</div>`; itemDetails.innerHTML = `<div class="details"> <p>No batches match the current filters.</p> </div>`; return; } filteredBatches.forEach(record => { const batchStatus = (record.batchstatussummary || record.abstatus || "").toLowerCase(); let batchIconName = ""; let batchIconColor = ""; if (batchStatus === "finished") { batchIconName = "check_circle"; batchIconColor = "#388E3C"; } else if (batchStatus === "errored") { batchIconName = "error"; batchIconColor = "#D32F2F"; } else if (batchStatus === "running") { batchIconName = "clock_loader_40"; batchIconColor = "#1976D2"; } const runStatus = (record.runsummary || "").toLowerCase(); let runIconName = ""; let runIconColor = ""; if (runStatus === "passed") { runIconName = "thumb_up"; runIconColor = "#388E3C"; } else if (runStatus === "passed with warnings") { runIconName = "thumb_up"; runIconColor = "#FF9800"; } else if (runStatus === "mixed" || runStatus === "mixed with warnings") { runIconName = "thumbs_up_down"; runIconColor = "#FF9800"; } else if (runStatus === "failed") { runIconName = "thumb_down"; runIconColor = "#D32F2F"; } const div = document.createElement("div"); div.className = "item"; div.dataset.batchId = record.abid; div.innerHTML = `<span class="work-icon">\ <i class="material-symbols-outlined" title="Batch status: ${batchStatus}" style="color: ${batchIconColor};">${batchIconName}</i>\ </span>\ <span class="status-icon">\ <i class="material-symbols-outlined" title="Run status: ${runStatus}" style="color: ${runIconColor};">${runIconName}</i>\ </span>\ <span class="batch-text">${record.abid}</span>`; div.addEventListener("click", () => { document.querySelectorAll(".item").forEach(i => i.classList.remove("active")); div.classList.add("active"); const totalRuns = record.abtottests ?? 0; const processedRuns = record.abprctests ?? 0; const started = record.abstartts && record.abstartts !== "0001-01-01-00.00.00.000000" ? record.abstartts : ""; const finished = record.abfinishts && record.abfinishts !== "0001-01-01-00.00.00.000000" ? record.abfinishts : ""; const envText = `${record.envdesc} (${record.abenv})`; const detailsHtml = `<table style="width: 100%; border-collapse: collapse;"> <colgroup> <col style="width:120px"> <col style="width:250px"> <col style="width:100px"> <col> <col style="width:100px"> <col style="width:200px"> </colgroup> <tr> <td><strong>Batch ID:</strong></td> <td colspan=3>${record.abid}</td> <td><strong>Status:</strong></td> <td style="text-transform:capitalize">${record.abstatus || "Unknown"}</td> </tr><tr> <td><strong>Started:</strong></td> <td>${started}</td> <td><strong>Finished:</strong></td> <td>${finished}</td> <td><strong>Processed:</strong></td> <td>${processedRuns} out of ${totalRuns}</td> </tr><tr> <td><strong>Environment:</strong></td> <td colspan="5">${envText}</td> </tr> <tr style="display:${record.aberror ? "table-row" : "none"}"> <td><strong>Error:</strong></td> <td colspan="5">${record.aberror}</td> </tr> </table> <hr> <div id="run-grid-container"> <p>Loading test runs...</p> </div>`; itemDetails.innerHTML = detailsHtml; fetchRunList(record.abid); }); itemList.appendChild(div); }); const params = new URLSearchParams(window.location.search); const targetBatchId = params.get("batchId"); if (targetBatchId) { const targetItem = Array.from(document.querySelectorAll(".item")) .find(item => item.dataset.batchId === targetBatchId); if (targetItem) { targetItem.click(); targetItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); return; } } document.querySelector(".item")?.click(); } // Listen for changes to filter checkboxes to refresh the batch list document.getElementById("filter-panel").addEventListener("change", () => { fetchBatchIds(); }); // Listen for changes to run summary checkboxes to refresh the batch list const runSummaryPanel = document.getElementById("run-summary-panel"); if (runSummaryPanel) { runSummaryPanel.addEventListener("change", () => { fetchBatchIds(); }); } // Fetch test runs for the given batch id function fetchRunList(batchId) { const payload = { batchId }; const headers = { "Content-Type": "application/json" }; if (sessionStorage["atrium-token"]) { headers["Authorization"] = "Basic " + sessionStorage["atrium-token"]; } fetch("/auto-testing/run-list-by-batch-id", { method: "POST", headers, body: JSON.stringify(payload) }) .then(res => res.json()) .then(data => { // Update batch info if provided if (data.batchInfo) { // Find and update the record in batchRecords cache const recordIndex = batchRecords.findIndex(r => r.abid === batchId); if (recordIndex >= 0) { // Update the cached record with fresh data Object.assign(batchRecords[recordIndex], { abstatus: data.batchInfo.abstatus || data.batchInfo.ABSTATUS, abtottests: data.batchInfo.abtottests || data.batchInfo.ABTOTTESTS, abprctests: data.batchInfo.abprctests || data.batchInfo.ABPRCTESTS, abenv: data.batchInfo.abenv || data.batchInfo.ABENV, aberror: data.batchInfo.aberror || data.batchInfo.ABERROR, abstartts: data.batchInfo.abstartts || data.batchInfo.ABSTARTTS, abfinishts: data.batchInfo.abfinishts || data.batchInfo.ABFINISHTS }); // Update the detail pane with fresh data const details = document.querySelector(".details"); if (details) { const record = batchRecords[recordIndex]; const totalRuns = record.abtottests ?? 0; const processedRuns = record.abprctests ?? 0; // Update status const statusP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Status:</strong>")); if (statusP) { statusP.innerHTML = `<strong>Status:</strong> ${record.abstatus || "Unknown"}`; } // Update total test runs processed const processedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Total test runs processed:</strong>")); if (processedP) { processedP.innerHTML = `<strong>Total test runs processed:</strong> ${processedRuns} out of ${totalRuns}`; } // Update started timestamp const startedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Started:</strong>")); if (startedP) { startedP.innerHTML = `<strong>Started:</strong> ${record.abstartts || "N/A"}`; } // Update finished timestamp const finishedP = Array.from(details.querySelectorAll("p")).find(p => p.innerHTML.includes("<strong>Finished:</strong>")); if (finishedP) { finishedP.innerHTML = `<strong>Finished:</strong> ${record.abfinishts || "N/A"}`; } // Update or add/remove error details const errorP = Array.from(details.querySelectorAll("p.error-text")).find(p => p.innerHTML.includes("<strong>Last error:</strong>")); if (record.aberror) { if (errorP) { errorP.innerHTML = `<strong>Last error:</strong> ${record.aberror}`; } else { // Insert error paragraph before the run-grid-container const container = document.getElementById("run-grid-container"); if (container) { const newErrorP = document.createElement("p"); newErrorP.className = "error-text"; newErrorP.innerHTML = `<strong>Last error:</strong> ${record.aberror}`; container.parentNode.insertBefore(newErrorP, container); } } } else if (errorP) { // Remove error paragraph if no error errorP.remove(); } } } } // Render the test runs grid const runs = data.runs || data; // Sort it based on last sorting start const field = Object.keys(runGridSortState)[0] || "arstart"; const sortDirection = runGridSortState[field] || "asc"; runs.sort((a, b) => { // Setup sortingValue for startTime if (!a.sortedStartTime) a.sortedStartTime = !a.arstart || a.arstart === "0001-01-01-00.00.00.000000" ? "9999999999999" : a.arstart; if (!b.sortedStartTime) b.sortedStartTime = !b.arstart || b.arstart === "0001-01-01-00.00.00.000000" ? "9999999999999" : b.arstart; const sortField = field === "arstart" ? "sortedStartTime" : field; const valA = a[sortField] || ""; const valB = b[sortField] || ""; if (sortDirection === "asc") { return valA > valB ? 1 : (valA < valB ? -1 : 0); } else { return valA < valB ? 1 : (valA > valB ? -1 : 0); } }); renderRunGrid(runs, runs.length); }) .catch(err => { console.error("Error fetching run list:", err); const container = document.getElementById("run-grid-container"); if (container) { container.innerHTML = `<p>Error loading test runs.</p>`; } }); } const runGridSortState = { arstart: "asc" }; const gridFilters = {}; function renderRunGrid(runData, total) { const container = document.getElementById("run-grid-container"); if (!container) return; // Build header rows dynamically using runGridSortState for sort indicators. const fields = [ { field: "testdesc", label: "Test description" }, { field: "navtext", label: "Item Description" }, { field: "arstart", label: "Start time" }, { field: "artime", label: "Run time" }, { field: "arappjob", label: "Job" }, { field: "arstatus", label: "Status" } ]; let headerRow = `<tr> <th class="action-column"></th>`; // reserved for action icons fields.forEach(f => { let indicator = ""; if (runGridSortState[f.field]) { indicator = runGridSortState[f.field] === "asc" ? " ▲" : " ▼"; } headerRow += `<th data-field="${f.field}">${f.label}${indicator}</th>`; }); headerRow += `</tr>`; // Build filtering row const filterRow = `<tr> <th class="action-column"></th> ${fields.map(f => `<th><input type="text" placeholder="Filter" data-field="${f.field}" value="${gridFilters[f.field] || ""}"></th>`).join("")} </tr>`; // Insert a colgroup to specify column widths let html = `<table id="run-grid"> <colgroup> <col> <!-- Action column --> <col> <!-- Test description --> <col> <!-- Navigation --> <col> <!-- Start time --> <col> <!-- Run time --> <col> <!-- Job --> <col> <!-- Status --> </colgroup> <thead> ${headerRow} ${filterRow} </thead> <tbody>`; runData.forEach(run => { const description = run.testdesc || "N/A"; const navDescription = run.navtext || "N/A"; // Parse the start time into date and time parts const rawStart = run.arstart || ""; let datePart = rawStart; let timePart = ""; if (rawStart === "0001-01-01-00.00.00.000000") { datePart = "--"; timePart = ""; } else if (rawStart) { const parts = rawStart.split("-"); if (parts.length >= 4) { datePart = parts.slice(0, 3).join("-"); // "YYYY-MM-DD" timePart = parts[3]; // "HH.MM.SS.ssssss" const timeParts = timePart.split("."); if (timeParts.length >= 3) { timePart = `${timeParts[0]}:${timeParts[1]}:${timeParts[2]}` + (timeParts.length > 3 ? `.${timeParts.slice(3).join(".")}` : ""); } } } let show = true; for (const field in gridFilters) { const value = gridFilters[field].toLowerCase(); if (value && !run[field].toLowerCase().includes(value)) { show = false; break; } } html += `<tr style="display:${show ? "" : "none"}"> <td class="action-cell"> <i class="material-symbols-outlined work-icon" title="Work with Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}','_blank');">list_alt</i> <i class="material-symbols-outlined work-icon" title="Work with Test Run" onclick="window.open('/auto-testing/work-with-test?runid=${run.arid}','_blank');">fact_check</i> <i class="material-symbols-outlined work-icon" title="Replay Test" onclick="window.open('/auto-testing/work-with-test?testid=${run.artid}&action=run&env=${run.arenv}','_blank');">play_circle</i> </td> <td>${description}</td> <td>${navDescription}</td> <td>${datePart} ${timePart}</td> <td>${run.artime === "0" ? "--" : run.artime}</td> <td>${run.arappjob || ""}</td> <td style="text-transform:capitalize">${run.arstatus || ""}</td> </tr>`; }); html += `</tbody></table>`; // Footer with total test runs; will be updated on filtering html += `<p id="total-runs-footer">Total test runs: ${total}</p>`; container.innerHTML = html; // Update visible count if filtering is active. function updateVisibleCount() { const tbody = document.querySelector("#run-grid tbody"); let visibleCount = 0; tbody.querySelectorAll("tr").forEach(tr => { if (tr.style.display !== "none") visibleCount++; }); const footer = document.getElementById("total-runs-footer"); if (visibleCount < runData.length) { footer.textContent = `Total test runs: ${total} (${visibleCount} currently displayed)`; } else { footer.textContent = `Total test runs: ${total}`; } } // Add event listeners for sorting headers. document.querySelectorAll("#run-grid thead th[data-field]").forEach(th => { th.style.cursor = "pointer"; th.addEventListener("click", function() { const field = th.getAttribute("data-field"); let sortDirection = "asc"; if (runGridSortState[field] === "asc") { sortDirection = "desc"; } // Clear sort state for all other columns. for (const key in runGridSortState) { if (key !== field) { delete runGridSortState[key]; } } runGridSortState[field] = sortDirection; // update global state // Clear visual sort attributes from all header cells document.querySelectorAll("#run-grid thead th[data-field]").forEach(header => { header.removeAttribute("data-sort"); }); th.setAttribute("data-sort", sortDirection); runData.sort((a, b) => { const sortField = field === "arstart" ? "sortedStartTime" : field; const valA = a[sortField] || ""; const valB = b[sortField] || ""; if (sortDirection === "asc") { return valA > valB ? 1 : (valA < valB ? -1 : 0); } else { return valA < valB ? 1 : (valA > valB ? -1 : 0); } }); renderRunGrid(runData, total); }); }); // Add event listeners for column filtering and update visible count. document.querySelectorAll("#run-grid thead input").forEach(input => { input.addEventListener("input", function() { // Build an object of filter values keyed by data-field name document.querySelectorAll("#run-grid thead input").forEach(inp => { const field = inp.getAttribute("data-field"); gridFilters[field] = inp.value.toLowerCase(); }); const tbody = document.querySelector("#run-grid tbody"); // Iterate over each row and check if it should be displayed tbody.querySelectorAll("tr").forEach(tr => { let show = true; tr.querySelectorAll("td").forEach((td, index) => { // If this cell contains an icon (the first column), skip filtering. if (td.querySelector(".work-icon")) return; // Otherwise, determine its corresponding data-field from the header const headerFields = document.querySelectorAll("#run-grid thead th[data-field]"); const cellIndex = Array.from(td.parentNode.children).indexOf(td); const field = headerFields[cellIndex - 1] ? headerFields[cellIndex - 1].getAttribute("data-field") : null; if (field && gridFilters[field] && !td.textContent.toLowerCase().includes(gridFilters[field])) { show = false; } }); tr.style.display = show ? "" : "none"; }); updateVisibleCount(); }); }); updateVisibleCount(); } if (window.location.search) { if (window.location.search.includes("batchId=")) { leftColumn.style.display = "none"; resizer.style.display = "none"; pageTitle.textContent = "Mass Test Details"; const urlParams = new URLSearchParams(window.location.search); const batchId = urlParams.get("batchId"); var refresh = function() { itemDetails.innerHTML = `<div class="details">Loading...</div>`; const headers = { "Content-Type": "application/json" }; if (sessionStorage["atrium-token"]) { headers["Authorization"] = "Basic " + sessionStorage["atrium-token"]; } fetch("/auto-testing/batch-list", { method: "POST", headers, body: JSON.stringify({ batchId: batchId }) }) .then(res => res.json()) .then(data => { renderBatchIds(data.data || []); }) .catch(err => { console.error("Error fetching batch:", err); displayErrorMessage("Unable to reach the server. Please check your connection."); }); }; refresh(); refreshButton.addEventListener("click", () => { refresh(); }); return; } } refreshButton.addEventListener("click", () => { fetchBatchIds(); }); // Initial render: fetch env list first, then batch groups fetchEnvList(() => { fetchBatchIds(); }); // Handle up/down arrow key navigation for list items document.addEventListener("keydown", function(e) { const items = document.querySelectorAll(".item"); if (!items.length) return; const currentIndex = Array.from(items).findIndex(item => item.classList.contains("active")); if (e.key === "ArrowDown") { e.preventDefault(); if (currentIndex < items.length - 1) { const nextItem = items[currentIndex + 1]; nextItem.click(); nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } else if (e.key === "ArrowUp") { e.preventDefault(); if (currentIndex > 0) { const prevItem = items[currentIndex - 1]; prevItem.click(); prevItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); } } }); });