profoundjs 7.20.4 → 7.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auto-testing/static/test-runs/index.html +46 -0
- package/auto-testing/static/test-runs/script.js +477 -0
- package/auto-testing/static/test-runs/style.css +237 -0
- package/htdocs/profoundui/proddata/css/markdown.css +23 -0
- package/htdocs/profoundui/proddata/css/plogic.css +4976 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.eot +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.svg +276 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.ttf +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.woff +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-700.woff2 +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.eot +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.svg +301 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.ttf +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.woff +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-italic.woff2 +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.eot +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.svg +281 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.ttf +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.woff +0 -0
- package/htdocs/profoundui/proddata/fonts/gudea-v10-latin-regular.woff2 +0 -0
- package/htdocs/profoundui/proddata/fonts/material_icons.woff2 +0 -0
- package/htdocs/profoundui/proddata/js/atrium.js +188 -186
- package/htdocs/profoundui/proddata/js/designer.js +3242 -3237
- package/htdocs/profoundui/proddata/js/genie.js +2169 -2168
- package/htdocs/profoundui/proddata/js/plogic.grids.js +326 -0
- package/htdocs/profoundui/proddata/js/runtime.js +1133 -1133
- package/htdocs/profoundui/proddata/js/signon.js +101 -99
- package/htdocs/profoundui/userdata/html/atrium_login.html +4 -1
- package/package.json +3 -1
- package/profound.jse +1 -1
- package/setup/completeInstall.js +7 -0
- package/setup/config.js +4 -1
- package/setup/modules/environment/README.md +17 -0
- package/setup/modules/environment/atenvcmds.json +1283 -0
- package/setup/modules/environment/atenvir.json +1643 -0
- package/setup/modules/environment/atuserenvirdefaults.json +1789 -0
- package/setup/modules/puiscreens.json +283 -0
- package/setup/pjsdist.savf +0 -0
- package/setup/setup.js +10 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
6
|
+
<title>Work with Auto-Submitted Test Runs</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css" />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet"> <!-- Material Icons added -->
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<!-- New overall page header -->
|
|
12
|
+
<header id="page-header">
|
|
13
|
+
<h1>Work with Auto-Submitted Test Runs</h1>
|
|
14
|
+
<button id="refresh-button">
|
|
15
|
+
<i class="material-symbols-outlined">refresh</i>
|
|
16
|
+
Refresh
|
|
17
|
+
</button>
|
|
18
|
+
</header>
|
|
19
|
+
<div class="container">
|
|
20
|
+
<div class="resizable" id="resizable-container">
|
|
21
|
+
<!-- Left column without the refresh button -->
|
|
22
|
+
<div class="column left" id="left-column">
|
|
23
|
+
<!-- New heading for the left column -->
|
|
24
|
+
<h2>Test Run Batches</h2>
|
|
25
|
+
<!-- Filter panel for statuses -->
|
|
26
|
+
<div id="filter-panel">
|
|
27
|
+
<label><input type="checkbox" value="finished" checked> Finished</label>
|
|
28
|
+
<label><input type="checkbox" value="errored" checked> Errored</label>
|
|
29
|
+
<label><input type="checkbox" value="running" checked> Running</label>
|
|
30
|
+
</div>
|
|
31
|
+
<!-- Items will be dynamically added here -->
|
|
32
|
+
<div id="item-list"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Resizer handle -->
|
|
36
|
+
<div class="resizer" id="resizer"></div>
|
|
37
|
+
|
|
38
|
+
<!-- Right column for item details -->
|
|
39
|
+
<div class="column right" id="item-details">
|
|
40
|
+
<p>Select an item to view details.</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<script src="script.js"></script>
|
|
45
|
+
</body>
|
|
46
|
+
</html>
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Description: Fetch and display a list of batch groups.
|
|
3
|
+
// When a batch group is clicked, its details are displayed in the right column.
|
|
4
|
+
// Querystring parameters:
|
|
5
|
+
// - batchid: The ID of the batch group to select and display details for. If not provided,
|
|
6
|
+
// the first batch group is selected.
|
|
7
|
+
|
|
8
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
9
|
+
if (!sessionStorage["atrium-token"]) {
|
|
10
|
+
// Redirect to login page if not authenticated, preserving current URL as a 'redirect' parameter
|
|
11
|
+
const currentUrl = window.location.pathname + window.location.search;
|
|
12
|
+
window.location.href = "/auto-testing/login?redirect=" + encodeURIComponent(currentUrl);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Grab all the elements we need
|
|
17
|
+
const leftColumn = document.getElementById("left-column");
|
|
18
|
+
// Set default width to 400px
|
|
19
|
+
leftColumn.style.flex = "0 0 400px";
|
|
20
|
+
|
|
21
|
+
const itemDetails = document.getElementById("item-details");
|
|
22
|
+
const refreshButton = document.getElementById("refresh-button");
|
|
23
|
+
const resizer = document.getElementById("resizer");
|
|
24
|
+
let isResizing = false;
|
|
25
|
+
|
|
26
|
+
// Disable text selection globally during drag
|
|
27
|
+
document.addEventListener("selectstart", (e) => {
|
|
28
|
+
if (isResizing) e.preventDefault();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Resizer functionality
|
|
32
|
+
resizer.addEventListener("mousedown", (e) => {
|
|
33
|
+
isResizing = true;
|
|
34
|
+
document.body.style.cursor = "ew-resize";
|
|
35
|
+
document.addEventListener("mousemove", resizeColumns);
|
|
36
|
+
document.addEventListener("mouseup", stopResizing);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function resizeColumns(e) {
|
|
40
|
+
if (!isResizing) return;
|
|
41
|
+
|
|
42
|
+
const containerRect = document.getElementById("resizable-container").getBoundingClientRect();
|
|
43
|
+
const leftWidth = e.clientX - containerRect.left;
|
|
44
|
+
const rightWidth = containerRect.right - e.clientX;
|
|
45
|
+
|
|
46
|
+
// Prevent columns from shrinking too much
|
|
47
|
+
if (leftWidth > 200 && rightWidth > 200) {
|
|
48
|
+
leftColumn.style.flex = `0 0 ${leftWidth}px`;
|
|
49
|
+
// Make the right column automatically fill the remaining space
|
|
50
|
+
itemDetails.style.flex = "1 1 auto";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stopResizing() {
|
|
55
|
+
isResizing = false;
|
|
56
|
+
document.body.style.cursor = "";
|
|
57
|
+
document.removeEventListener("mousemove", resizeColumns);
|
|
58
|
+
document.removeEventListener("mouseup", stopResizing);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Show error message to the user in the details pane
|
|
62
|
+
function displayErrorMessage(message) {
|
|
63
|
+
itemDetails.innerHTML = `<div class="details error">
|
|
64
|
+
<p>${message}</p>
|
|
65
|
+
</div>`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Get batch group info from server
|
|
69
|
+
function fetchBatchIds() {
|
|
70
|
+
// Insert a loading spinner before fetching data
|
|
71
|
+
const itemList = document.getElementById("item-list");
|
|
72
|
+
itemList.innerHTML = `<div class="spinner"></div>`;
|
|
73
|
+
|
|
74
|
+
const headers = {
|
|
75
|
+
"Content-Type": "application/json"
|
|
76
|
+
};
|
|
77
|
+
if (sessionStorage["atrium-token"]) {
|
|
78
|
+
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
|
|
79
|
+
};
|
|
80
|
+
fetch("/auto-testing/batch-list", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers
|
|
83
|
+
})
|
|
84
|
+
.then(res => res.json())
|
|
85
|
+
.then(data => {
|
|
86
|
+
renderBatchIds(data.data);
|
|
87
|
+
})
|
|
88
|
+
.catch(err => {
|
|
89
|
+
console.error("Error fetching batch groups:", err);
|
|
90
|
+
displayErrorMessage("Unable to reach the server. Please check your connection.");
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderBatchIds(batchIds) {
|
|
95
|
+
// Read filter values from checkboxes
|
|
96
|
+
const filterPanel = document.getElementById("filter-panel");
|
|
97
|
+
const checkedStatuses = Array.from(filterPanel.querySelectorAll("input[type=checkbox]:checked"))
|
|
98
|
+
.map(checkbox => checkbox.value);
|
|
99
|
+
|
|
100
|
+
// Group batches by their "batch id" (abid)
|
|
101
|
+
const batchesMap = {};
|
|
102
|
+
batchIds.forEach(obj => {
|
|
103
|
+
const batchKey = obj.abid;
|
|
104
|
+
// Only include the batch if its status is in the checkedStatuses
|
|
105
|
+
const status = (obj.abstatus || "").toLowerCase();
|
|
106
|
+
if (!checkedStatuses.includes(status)) {
|
|
107
|
+
return; // skip object
|
|
108
|
+
}
|
|
109
|
+
if (!batchesMap[batchKey]) {
|
|
110
|
+
batchesMap[batchKey] = [];
|
|
111
|
+
}
|
|
112
|
+
batchesMap[batchKey].push(obj);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const itemList = document.getElementById("item-list");
|
|
116
|
+
itemList.innerHTML = ""; // Clear the list
|
|
117
|
+
|
|
118
|
+
// Iterate over each unique batch group key
|
|
119
|
+
Object.keys(batchesMap).forEach(groupKey => {
|
|
120
|
+
const batchData = batchesMap[groupKey];
|
|
121
|
+
const batchStatus = (batchData[0].batchstatussummary || "").toLowerCase();
|
|
122
|
+
let batchIconName = "";
|
|
123
|
+
let batchIconColor = "";
|
|
124
|
+
if (batchStatus === "finished") {
|
|
125
|
+
batchIconName = "check_circle";
|
|
126
|
+
batchIconColor = "#388E3C";
|
|
127
|
+
}
|
|
128
|
+
else if (batchStatus === "errored") {
|
|
129
|
+
batchIconName = "error";
|
|
130
|
+
batchIconColor = "#D32F2F";
|
|
131
|
+
}
|
|
132
|
+
else if (batchStatus === "running") {
|
|
133
|
+
batchIconName = "clock_loader_40";
|
|
134
|
+
batchIconColor = "#1976D2";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const runStatus = (batchData[0].runsummary || "").toLowerCase();
|
|
138
|
+
let runIconName = "";
|
|
139
|
+
let runIconColor = "";
|
|
140
|
+
if (runStatus === "passed") {
|
|
141
|
+
runIconName = "thumb_up";
|
|
142
|
+
runIconColor = "#388E3C";
|
|
143
|
+
}
|
|
144
|
+
else if (runStatus === "passed with warnings") {
|
|
145
|
+
runIconName = "thumb_up";
|
|
146
|
+
runIconColor = "#FF9800";
|
|
147
|
+
}
|
|
148
|
+
else if (runStatus === "mixed") {
|
|
149
|
+
runIconName = "thumbs_up_down";
|
|
150
|
+
runIconColor = "#FF9800";
|
|
151
|
+
}
|
|
152
|
+
else if (runStatus === "failed") {
|
|
153
|
+
runIconName = "thumb_down";
|
|
154
|
+
runIconColor = "#D32F2F";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const div = document.createElement("div");
|
|
158
|
+
div.className = "item";
|
|
159
|
+
div.innerHTML = `<span class="work-icon">\
|
|
160
|
+
<i class="material-symbols-outlined" title="Batch status: ${batchStatus}" style="color: ${batchIconColor};">${batchIconName}</i>\
|
|
161
|
+
</span>\
|
|
162
|
+
<span class="status-icon">\
|
|
163
|
+
<i class="material-symbols-outlined" title="Run status: ${runStatus}" style="color: ${runIconColor};">${runIconName}</i>\
|
|
164
|
+
</span>\
|
|
165
|
+
<span class="batch-text">${groupKey}</span>`;
|
|
166
|
+
div.addEventListener("click", () => {
|
|
167
|
+
document.querySelectorAll(".item").forEach(i => i.classList.remove("active"));
|
|
168
|
+
div.classList.add("active");
|
|
169
|
+
// Build details: list each batch with its segment
|
|
170
|
+
const batchData = batchesMap[groupKey];
|
|
171
|
+
const detailsHtml = `<div class="details">
|
|
172
|
+
<p><strong>Batch ID:</strong> ${groupKey}</p>
|
|
173
|
+
<p><strong>Segments:</strong></p>
|
|
174
|
+
<ul>
|
|
175
|
+
${batchData.map(batch => `<li>${String(batch.abseg).padStart(2, "0")} - Status: ${batch.abstatus}</li>`).join("")}
|
|
176
|
+
</ul>
|
|
177
|
+
<div id="run-grid-container">
|
|
178
|
+
<p>Loading test runs...</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>`;
|
|
181
|
+
itemDetails.innerHTML = detailsHtml;
|
|
182
|
+
// Fetch and display run list for this batch group
|
|
183
|
+
fetchRunList(groupKey);
|
|
184
|
+
});
|
|
185
|
+
itemList.appendChild(div);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Check for batchId in URL and simulate click on it
|
|
189
|
+
const params = new URLSearchParams(window.location.search);
|
|
190
|
+
const targetBatchId = params.get("batchId"); // new parameter
|
|
191
|
+
if (targetBatchId) {
|
|
192
|
+
const targetItem = Array.from(document.querySelectorAll(".item"))
|
|
193
|
+
.find(item => item.querySelector(".batch-text")?.textContent.trim() === targetBatchId);
|
|
194
|
+
if (targetItem) {
|
|
195
|
+
targetItem.click();
|
|
196
|
+
targetItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
197
|
+
// If a batchSegment is also provided, set the segment filter
|
|
198
|
+
const targetBatchSegment = params.get("batchSegment");
|
|
199
|
+
if (targetBatchSegment) {
|
|
200
|
+
const segmentInput = document.querySelector("#run-grid thead input[data-field='arseg']");
|
|
201
|
+
if (segmentInput) {
|
|
202
|
+
segmentInput.value = targetBatchSegment;
|
|
203
|
+
segmentInput.dispatchEvent(new Event("input"));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
document.querySelector(".item")?.click();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
document.querySelector(".item")?.click();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Listen for changes to filter checkboxes to refresh the batch list
|
|
217
|
+
document.getElementById("filter-panel").addEventListener("change", () => {
|
|
218
|
+
fetchBatchIds();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Fetch test runs for the given batch id
|
|
222
|
+
function fetchRunList(batchId) {
|
|
223
|
+
const payload = { batchId };
|
|
224
|
+
const headers = {
|
|
225
|
+
"Content-Type": "application/json"
|
|
226
|
+
};
|
|
227
|
+
if (sessionStorage["atrium-token"]) {
|
|
228
|
+
headers["Authorization"] = "Basic " + sessionStorage["atrium-token"];
|
|
229
|
+
}
|
|
230
|
+
fetch("/auto-testing/run-list-by-batch-id", {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers,
|
|
233
|
+
body: JSON.stringify(payload)
|
|
234
|
+
})
|
|
235
|
+
.then(res => res.json())
|
|
236
|
+
.then(data => {
|
|
237
|
+
renderRunGrid(data, data.length);
|
|
238
|
+
})
|
|
239
|
+
.catch(err => {
|
|
240
|
+
console.error("Error fetching run list:", err);
|
|
241
|
+
const container = document.getElementById("run-grid-container");
|
|
242
|
+
if (container) {
|
|
243
|
+
container.innerHTML = `<p>Error loading test runs.</p>`;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const runGridSortState = {}; // global variable to store sort state per field
|
|
249
|
+
|
|
250
|
+
function renderRunGrid(runData, total) {
|
|
251
|
+
const container = document.getElementById("run-grid-container");
|
|
252
|
+
if (!container) return;
|
|
253
|
+
|
|
254
|
+
// Build header rows dynamically using runGridSortState for sort indicators.
|
|
255
|
+
const fields = [
|
|
256
|
+
{ field: "testdesc", label: "Test description" },
|
|
257
|
+
{ field: "arseg", label: "Segment" },
|
|
258
|
+
{ field: "navtext", label: "Navigation<br>Item Description" },
|
|
259
|
+
{ field: "arstart", label: "Start time" },
|
|
260
|
+
{ field: "artime", label: "Run time" },
|
|
261
|
+
{ field: "arappjob", label: "Job" },
|
|
262
|
+
{ field: "arstatus", label: "Status" }
|
|
263
|
+
];
|
|
264
|
+
let headerRow = `<tr>
|
|
265
|
+
<th class="action-column"></th>`; // reserved for action icons
|
|
266
|
+
fields.forEach(f => {
|
|
267
|
+
let indicator = "";
|
|
268
|
+
if (runGridSortState[f.field]) {
|
|
269
|
+
indicator = runGridSortState[f.field] === "asc" ? " ▲" : " ▼";
|
|
270
|
+
}
|
|
271
|
+
headerRow += `<th data-field="${f.field}">${f.label}${indicator}</th>`;
|
|
272
|
+
});
|
|
273
|
+
headerRow += `</tr>`;
|
|
274
|
+
|
|
275
|
+
// Build filtering row
|
|
276
|
+
const filterRow = `<tr>
|
|
277
|
+
<th class="action-column"></th>
|
|
278
|
+
<th><input type="text" placeholder="Filter" data-field="desc"></th>
|
|
279
|
+
<th><input type="text" placeholder="Filter" data-field="arseg"></th>
|
|
280
|
+
<th><input type="text" placeholder="Filter" data-field="navtext"></th>
|
|
281
|
+
<th><input type="text" placeholder="Filter" data-field="arstart"></th>
|
|
282
|
+
<th><input type="text" placeholder="Filter" data-field="artime"></th>
|
|
283
|
+
<th><input type="text" placeholder="Filter" data-field="arappjob"></th>
|
|
284
|
+
<th><input type="text" placeholder="Filter" data-field="arstatus"></th>
|
|
285
|
+
</tr>`;
|
|
286
|
+
|
|
287
|
+
// Insert a colgroup to specify column widths
|
|
288
|
+
let html = `<table id="run-grid">
|
|
289
|
+
<colgroup>
|
|
290
|
+
<col> <!-- Action column -->
|
|
291
|
+
<col> <!-- Test description -->
|
|
292
|
+
<col style="max-width: 100px; width: 100px;"> <!-- Segment column -->
|
|
293
|
+
<col> <!-- Navigation -->
|
|
294
|
+
<col> <!-- Start time -->
|
|
295
|
+
<col> <!-- Run time -->
|
|
296
|
+
<col> <!-- Job -->
|
|
297
|
+
<col> <!-- Status -->
|
|
298
|
+
</colgroup>
|
|
299
|
+
<thead>
|
|
300
|
+
${headerRow}
|
|
301
|
+
${filterRow}
|
|
302
|
+
</thead>
|
|
303
|
+
<tbody>`;
|
|
304
|
+
|
|
305
|
+
runData.forEach(run => {
|
|
306
|
+
const description = run.testdesc || "N/A";
|
|
307
|
+
const segment = run.arseg || "";
|
|
308
|
+
const navDescription = run.navtext || "N/A";
|
|
309
|
+
// Parse the start time into date and time parts
|
|
310
|
+
const rawStart = run.arstart || "";
|
|
311
|
+
let datePart = rawStart;
|
|
312
|
+
let timePart = "";
|
|
313
|
+
if (rawStart) {
|
|
314
|
+
const parts = rawStart.split("-");
|
|
315
|
+
if (parts.length >= 4) {
|
|
316
|
+
datePart = parts.slice(0, 3).join("-"); // "YYYY-MM-DD"
|
|
317
|
+
timePart = parts[3]; // "HH.MM.SS.ssssss"
|
|
318
|
+
const timeParts = timePart.split(".");
|
|
319
|
+
if (timeParts.length >= 3) {
|
|
320
|
+
timePart = `${timeParts[0]}:${timeParts[1]}:${timeParts[2]}` +
|
|
321
|
+
(timeParts.length > 3 ? `.${timeParts.slice(3).join(".")}` : "");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
html += `<tr>
|
|
326
|
+
<td class="action-cell">
|
|
327
|
+
<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>
|
|
328
|
+
<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>
|
|
329
|
+
<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>
|
|
330
|
+
</td>
|
|
331
|
+
<td>${description}</td>
|
|
332
|
+
<td>${segment}</td>
|
|
333
|
+
<td>${navDescription}</td>
|
|
334
|
+
<td data-sort="${rawStart}"><span class="date">${datePart}</span><br><span class="time">${timePart}</span></td>
|
|
335
|
+
<td>${run.artime === "0" ? "--" : run.artime}</td>
|
|
336
|
+
<td>${run.arappjob || ""}</td>
|
|
337
|
+
<td>${run.arstatus || ""}</td>
|
|
338
|
+
</tr>`;
|
|
339
|
+
});
|
|
340
|
+
html += `</tbody></table>`;
|
|
341
|
+
// Footer with total test runs; will be updated on filtering
|
|
342
|
+
html += `<p id="total-runs-footer">Total test runs: ${total}</p>`;
|
|
343
|
+
container.innerHTML = html;
|
|
344
|
+
|
|
345
|
+
// If both 'batchId' and 'batchSegment' query string parameters exist, and the segment
|
|
346
|
+
// filter has not been applied yet, set the segment filter
|
|
347
|
+
if (!window.batchSegmentApplied) {
|
|
348
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
349
|
+
if (urlParams.get("batchId") && urlParams.get("batchSegment")) {
|
|
350
|
+
const segmentFilterInput = document.querySelector("#run-grid thead input[data-field='arseg']");
|
|
351
|
+
if (segmentFilterInput) {
|
|
352
|
+
segmentFilterInput.value = urlParams.get("batchSegment");
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
segmentFilterInput.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
|
355
|
+
}, 0);
|
|
356
|
+
window.batchSegmentApplied = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Update visible count if filtering is active.
|
|
362
|
+
function updateVisibleCount() {
|
|
363
|
+
const tbody = document.querySelector("#run-grid tbody");
|
|
364
|
+
let visibleCount = 0;
|
|
365
|
+
tbody.querySelectorAll("tr").forEach(tr => {
|
|
366
|
+
if (tr.style.display !== "none") visibleCount++;
|
|
367
|
+
});
|
|
368
|
+
const footer = document.getElementById("total-runs-footer");
|
|
369
|
+
if (visibleCount < runData.length) {
|
|
370
|
+
footer.textContent = `Total test runs: ${total} (${visibleCount} currently displayed)`;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
footer.textContent = `Total test runs: ${total}`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Add event listeners for sorting headers.
|
|
378
|
+
document.querySelectorAll("#run-grid thead th[data-field]").forEach(th => {
|
|
379
|
+
th.style.cursor = "pointer";
|
|
380
|
+
th.addEventListener("click", function() {
|
|
381
|
+
const field = th.getAttribute("data-field");
|
|
382
|
+
let sortDirection = "asc";
|
|
383
|
+
if (runGridSortState[field] === "asc") {
|
|
384
|
+
sortDirection = "desc";
|
|
385
|
+
}
|
|
386
|
+
// Clear sort state for all other columns.
|
|
387
|
+
for (const key in runGridSortState) {
|
|
388
|
+
if (key !== field) {
|
|
389
|
+
delete runGridSortState[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
runGridSortState[field] = sortDirection; // update global state
|
|
393
|
+
|
|
394
|
+
// Clear visual sort attributes from all header cells
|
|
395
|
+
document.querySelectorAll("#run-grid thead th[data-field]").forEach(header => {
|
|
396
|
+
header.removeAttribute("data-sort");
|
|
397
|
+
});
|
|
398
|
+
th.setAttribute("data-sort", sortDirection);
|
|
399
|
+
|
|
400
|
+
runData.sort((a, b) => {
|
|
401
|
+
const valA = a[field] || "";
|
|
402
|
+
const valB = b[field] || "";
|
|
403
|
+
if (sortDirection === "asc") {
|
|
404
|
+
return valA > valB ? 1 : (valA < valB ? -1 : 0);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
return valA < valB ? 1 : (valA > valB ? -1 : 0);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
renderRunGrid(runData, total);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Add event listeners for column filtering and update visible count.
|
|
415
|
+
document.querySelectorAll("#run-grid thead input").forEach(input => {
|
|
416
|
+
input.addEventListener("input", function() {
|
|
417
|
+
const filters = {};
|
|
418
|
+
// Build an object of filter values keyed by data-field name
|
|
419
|
+
document.querySelectorAll("#run-grid thead input").forEach(inp => {
|
|
420
|
+
const field = inp.getAttribute("data-field");
|
|
421
|
+
filters[field] = inp.value.toLowerCase();
|
|
422
|
+
});
|
|
423
|
+
const tbody = document.querySelector("#run-grid tbody");
|
|
424
|
+
// Iterate over each row and check if it should be displayed
|
|
425
|
+
tbody.querySelectorAll("tr").forEach(tr => {
|
|
426
|
+
let show = true;
|
|
427
|
+
tr.querySelectorAll("td").forEach((td, index) => {
|
|
428
|
+
// If this cell contains an icon (the first three columns), skip filtering.
|
|
429
|
+
if (td.querySelector(".work-icon")) return;
|
|
430
|
+
|
|
431
|
+
// Otherwise, determine its corresponding data-field from the header
|
|
432
|
+
const headerFields = document.querySelectorAll("#run-grid thead th[data-field]");
|
|
433
|
+
const cellIndex = Array.from(td.parentNode.children).indexOf(td);
|
|
434
|
+
// Adjust for icon columns -- they don't have headers or filter fields so column indexes get thrown off
|
|
435
|
+
const field = headerFields[cellIndex - 1] ? headerFields[cellIndex - 1].getAttribute("data-field") : null;
|
|
436
|
+
if (field && filters[field] && !td.textContent.toLowerCase().includes(filters[field])) {
|
|
437
|
+
show = false;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
tr.style.display = show ? "" : "none";
|
|
441
|
+
});
|
|
442
|
+
updateVisibleCount();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Update refresh button to re-fetch data
|
|
448
|
+
refreshButton.addEventListener("click", () => {
|
|
449
|
+
fetchBatchIds();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Initial render: fetch batch groups on load
|
|
453
|
+
fetchBatchIds();
|
|
454
|
+
|
|
455
|
+
// Handle up/down arrow key navigation for list items
|
|
456
|
+
document.addEventListener("keydown", function(e) {
|
|
457
|
+
const items = document.querySelectorAll(".item");
|
|
458
|
+
if (!items.length) return;
|
|
459
|
+
const currentIndex = Array.from(items).findIndex(item => item.classList.contains("active"));
|
|
460
|
+
if (e.key === "ArrowDown") {
|
|
461
|
+
e.preventDefault();
|
|
462
|
+
if (currentIndex < items.length - 1) {
|
|
463
|
+
const nextItem = items[currentIndex + 1];
|
|
464
|
+
nextItem.click();
|
|
465
|
+
nextItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (e.key === "ArrowUp") {
|
|
469
|
+
e.preventDefault();
|
|
470
|
+
if (currentIndex > 0) {
|
|
471
|
+
const prevItem = items[currentIndex - 1];
|
|
472
|
+
prevItem.click();
|
|
473
|
+
prevItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|