UNPKG

profoundjs

Version:

Profound.js Framework and Server

478 lines (440 loc) 18.4 kB
"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 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>`; } // 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"]; }; fetch("/auto-testing/batch-list", { method: "POST", headers }) .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) { // Read filter values from checkboxes const filterPanel = document.getElementById("filter-panel"); const checkedStatuses = Array.from(filterPanel.querySelectorAll("input[type=checkbox]:checked")) .map(checkbox => checkbox.value); // Group batches by their "batch id" (abid) const batchesMap = {}; batchIds.forEach(obj => { const batchKey = obj.abid; // Only include the batch if its status is in the checkedStatuses const status = (obj.abstatus || "").toLowerCase(); if (!checkedStatuses.includes(status)) { return; // skip object } if (!batchesMap[batchKey]) { batchesMap[batchKey] = []; } batchesMap[batchKey].push(obj); }); const itemList = document.getElementById("item-list"); itemList.innerHTML = ""; // Clear the list // Iterate over each unique batch group key Object.keys(batchesMap).forEach(groupKey => { const batchData = batchesMap[groupKey]; const batchStatus = (batchData[0].batchstatussummary || "").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 = (batchData[0].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") { runIconName = "thumbs_up_down"; runIconColor = "#FF9800"; } else if (runStatus === "failed") { runIconName = "thumb_down"; runIconColor = "#D32F2F"; } const div = document.createElement("div"); div.className = "item"; 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">${groupKey}</span>`; div.addEventListener("click", () => { document.querySelectorAll(".item").forEach(i => i.classList.remove("active")); div.classList.add("active"); // Build details: list each batch with its segment const batchData = batchesMap[groupKey]; const detailsHtml = `<div class="details"> <p><strong>Batch ID:</strong> ${groupKey}</p> <p><strong>Segments:</strong></p> <ul> ${batchData.map(batch => `<li>${String(batch.abseg).padStart(2, "0")} - Status: ${batch.abstatus}</li>`).join("")} </ul> <div id="run-grid-container"> <p>Loading test runs...</p> </div> </div>`; itemDetails.innerHTML = detailsHtml; // Fetch and display run list for this batch group fetchRunList(groupKey); }); itemList.appendChild(div); }); // Check for batchId in URL and simulate click on it const params = new URLSearchParams(window.location.search); const targetBatchId = params.get("batchId"); // new parameter if (targetBatchId) { const targetItem = Array.from(document.querySelectorAll(".item")) .find(item => item.querySelector(".batch-text")?.textContent.trim() === targetBatchId); if (targetItem) { targetItem.click(); targetItem.scrollIntoView({ behavior: "smooth", block: "nearest" }); // If a batchSegment is also provided, set the segment filter const targetBatchSegment = params.get("batchSegment"); if (targetBatchSegment) { const segmentInput = document.querySelector("#run-grid thead input[data-field='arseg']"); if (segmentInput) { segmentInput.value = targetBatchSegment; segmentInput.dispatchEvent(new Event("input")); } } } else { document.querySelector(".item")?.click(); } } else { document.querySelector(".item")?.click(); } } // Listen for changes to filter checkboxes to refresh the batch list document.getElementById("filter-panel").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 => { renderRunGrid(data, data.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 = {}; // global variable to store sort state per field 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: "arseg", label: "Segment" }, { field: "navtext", label: "Navigation<br>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> <th><input type="text" placeholder="Filter" data-field="desc"></th> <th><input type="text" placeholder="Filter" data-field="arseg"></th> <th><input type="text" placeholder="Filter" data-field="navtext"></th> <th><input type="text" placeholder="Filter" data-field="arstart"></th> <th><input type="text" placeholder="Filter" data-field="artime"></th> <th><input type="text" placeholder="Filter" data-field="arappjob"></th> <th><input type="text" placeholder="Filter" data-field="arstatus"></th> </tr>`; // Insert a colgroup to specify column widths let html = `<table id="run-grid"> <colgroup> <col> <!-- Action column --> <col> <!-- Test description --> <col style="max-width: 100px; width: 100px;"> <!-- Segment column --> <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 segment = run.arseg || ""; 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) { 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(".")}` : ""); } } } html += `<tr> <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>${segment}</td> <td>${navDescription}</td> <td data-sort="${rawStart}"><span class="date">${datePart}</span><br><span class="time">${timePart}</span></td> <td>${run.artime === "0" ? "--" : run.artime}</td> <td>${run.arappjob || ""}</td> <td>${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; // If both 'batchId' and 'batchSegment' query string parameters exist, and the segment // filter has not been applied yet, set the segment filter if (!window.batchSegmentApplied) { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get("batchId") && urlParams.get("batchSegment")) { const segmentFilterInput = document.querySelector("#run-grid thead input[data-field='arseg']"); if (segmentFilterInput) { segmentFilterInput.value = urlParams.get("batchSegment"); setTimeout(() => { segmentFilterInput.dispatchEvent(new Event("input", { bubbles: true, cancelable: true })); }, 0); window.batchSegmentApplied = true; } } } // 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 valA = a[field] || ""; const valB = b[field] || ""; 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() { const filters = {}; // 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"); filters[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 three columns), 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); // Adjust for icon columns -- they don't have headers or filter fields so column indexes get thrown off const field = headerFields[cellIndex - 1] ? headerFields[cellIndex - 1].getAttribute("data-field") : null; if (field && filters[field] && !td.textContent.toLowerCase().includes(filters[field])) { show = false; } }); tr.style.display = show ? "" : "none"; }); updateVisibleCount(); }); }); } // Update refresh button to re-fetch data refreshButton.addEventListener("click", () => { fetchBatchIds(); }); // Initial render: fetch batch groups on load 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" }); } } }); });