profoundjs
Version:
Profound.js Framework and Server
478 lines (440 loc) • 18.4 kB
JavaScript
;
// 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" });
}
}
});
});