2024-10-01 19:19:35 +00:00
|
|
|
<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
<title>praktika report</title>
|
|
|
|
<link rel="icon" href="https://w4z3pajszlbkfcw2wcylfei5km0xmwag.lambda-url.us-east-1.on.aws/" type="image/x-icon">
|
|
|
|
<style>
|
2024-10-23 19:59:42 +00:00
|
|
|
|
|
|
|
/* Default (Day Theme) */
|
|
|
|
:root {
|
|
|
|
--background-color: white;
|
|
|
|
--text-color: #000;
|
|
|
|
--tile-background: #f9f9f9;
|
|
|
|
--footer-background: #f1f1f1;
|
|
|
|
--footer-text-color: #000;
|
|
|
|
--status-width: 300px;
|
|
|
|
}
|
|
|
|
|
|
|
|
body {
|
|
|
|
background-color: var(--background-color);
|
|
|
|
color: var(--text-color);
|
|
|
|
height: 100%;
|
|
|
|
margin: 0;
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
2024-10-24 11:17:00 +00:00
|
|
|
font-family: 'IBM Plex Mono Condensed', monospace, sans-serif;
|
|
|
|
--header-background-color: #f4f4f4;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
body.night-theme {
|
|
|
|
--background-color: #1F1F1C;
|
|
|
|
--text-color: #fff;
|
|
|
|
--tile-background: black;
|
2024-10-24 11:17:00 +00:00
|
|
|
--header-background-color: #1F1F1C;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#info-container {
|
|
|
|
margin-left: calc(var(--status-width) + 20px);
|
|
|
|
margin-bottom: 10px;
|
|
|
|
background-color: var(--tile-background);
|
|
|
|
padding: 10px;
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
#status-container {
|
|
|
|
position: fixed;
|
|
|
|
top: 0;
|
|
|
|
bottom: 0;
|
|
|
|
left: 0;
|
|
|
|
width: var(--status-width);
|
|
|
|
background-color: var(--tile-background);
|
|
|
|
padding: 20px;
|
|
|
|
box-sizing: border-box;
|
|
|
|
font-size: 18px;
|
2024-10-24 11:17:00 +00:00
|
|
|
margin: 0;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
#status-container a {
|
|
|
|
color: #007bff;
|
|
|
|
text-decoration: underline;
|
|
|
|
font-weight: bold;
|
2024-10-23 19:59:42 +00:00
|
|
|
cursor: pointer;
|
2024-10-24 11:17:00 +00:00
|
|
|
display: inline-block;
|
|
|
|
margin-top: 5px;
|
|
|
|
margin-left: 20px;
|
|
|
|
padding: 2px 0;
|
|
|
|
font-size: 0.8em;
|
|
|
|
}
|
|
|
|
|
|
|
|
#status-container a:hover {
|
|
|
|
color: #0056b3;
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
.key-value-pair {
|
|
|
|
display: flex; /* Enable Flexbox for alignment */
|
|
|
|
justify-content: space-between; /* Distribute space between key and value */
|
|
|
|
margin-bottom: 20px; /* Add space between each pair */
|
|
|
|
}
|
|
|
|
|
|
|
|
.json-key {
|
|
|
|
font-weight: bold;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
.json-value {
|
|
|
|
font-weight: normal;
|
|
|
|
font-family: 'Source Code Pro', monospace, sans-serif;
|
|
|
|
letter-spacing: -0.5px;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2024-10-26 17:35:33 +00:00
|
|
|
.dropdown-value {
|
|
|
|
width: 100px;
|
|
|
|
font-weight: normal;
|
|
|
|
font-family: inherit;
|
|
|
|
background-color: transparent;
|
|
|
|
color: inherit;
|
|
|
|
/*border: none;*/
|
|
|
|
/*outline: none;*/
|
|
|
|
/*cursor: pointer;*/
|
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#result-container {
|
|
|
|
background-color: var(--tile-background);
|
|
|
|
margin-left: calc(var(--status-width) + 20px);
|
2024-11-13 12:24:32 +00:00
|
|
|
padding: 0;
|
2024-10-23 19:59:42 +00:00
|
|
|
box-sizing: border-box;
|
|
|
|
text-align: center;
|
|
|
|
font-size: 18px;
|
|
|
|
font-weight: normal;
|
|
|
|
flex-grow: 1;
|
2024-11-13 12:24:32 +00:00
|
|
|
margin-bottom: 40px;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
#footer {
|
2024-10-23 19:59:42 +00:00
|
|
|
padding: 10px;
|
2024-10-01 19:19:35 +00:00
|
|
|
position: fixed;
|
|
|
|
bottom: 0;
|
|
|
|
left: 0;
|
|
|
|
right: 0;
|
|
|
|
background-color: #1F1F1C;
|
|
|
|
color: white;
|
|
|
|
font-size: 14px;
|
|
|
|
display: flex;
|
2024-10-23 19:59:42 +00:00
|
|
|
justify-content: space-between; /* Ensure the .left expands, and .right and .settings are aligned to the right */
|
2024-10-01 19:19:35 +00:00
|
|
|
align-items: center;
|
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#footer a {
|
|
|
|
color: white;
|
|
|
|
text-decoration: none;
|
|
|
|
}
|
|
|
|
|
|
|
|
#footer .left {
|
|
|
|
flex-grow: 1; /* Takes up all the available space */
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* make some space around '/' in the navigation line */
|
|
|
|
#footer .left span.separator {
|
|
|
|
margin-left: 5px;
|
|
|
|
margin-right: 5px;
|
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#footer .right, #footer .settings {
|
2024-10-01 19:19:35 +00:00
|
|
|
display: flex;
|
2024-10-23 19:59:42 +00:00
|
|
|
align-items: center;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#footer .right a::before {
|
|
|
|
content: "#";
|
|
|
|
margin-left: 10px;
|
|
|
|
color: #e0e0e0;
|
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#footer .right::before, #footer .settings::before {
|
|
|
|
content: "|"; /* Add separator before right and settings sections */
|
|
|
|
margin-left: 10px;
|
|
|
|
margin-right: 10px;
|
|
|
|
color: #e0e0e0;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#theme-toggle {
|
|
|
|
cursor: pointer;
|
|
|
|
font-size: 20px;
|
|
|
|
color: white;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#theme-toggle:hover {
|
|
|
|
color: #e0e0e0;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
#footer a:hover {
|
|
|
|
text-decoration: underline;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#links {
|
|
|
|
margin-top: 10px;
|
|
|
|
padding: 15px;
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
border-radius: 5px;
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
}
|
|
|
|
|
|
|
|
#links a {
|
|
|
|
display: block;
|
|
|
|
margin-bottom: 5px;
|
|
|
|
padding: 5px 10px;
|
|
|
|
background-color: #D5D5D5;
|
|
|
|
color: black;
|
|
|
|
text-decoration: none;
|
|
|
|
border-radius: 5px;
|
|
|
|
}
|
|
|
|
|
|
|
|
#links a:hover {
|
|
|
|
background-color: #D5D5D5;
|
|
|
|
}
|
|
|
|
|
|
|
|
table {
|
|
|
|
width: 100%;
|
|
|
|
border-collapse: collapse;
|
|
|
|
}
|
|
|
|
|
|
|
|
th.name-column, td.name-column {
|
2024-10-29 21:09:03 +00:00
|
|
|
min-width: 350px;
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
th.status-column, td.status-column {
|
|
|
|
max-width: 100px; /* Set the maximum width for the column */
|
|
|
|
white-space: nowrap; /* Prevent text from wrapping */
|
|
|
|
overflow: hidden; /* Hide the overflowed text */
|
|
|
|
text-overflow: ellipsis; /* Show ellipsis (...) for overflowed text */
|
|
|
|
}
|
|
|
|
|
|
|
|
th.time-column, td.time-column {
|
|
|
|
max-width: 120px; /* Set the maximum width for the column */
|
|
|
|
white-space: nowrap; /* Prevent text from wrapping */
|
|
|
|
text-align: right;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
th.info-column, td.info-column {
|
2024-10-23 19:59:42 +00:00
|
|
|
width: 100%; /* Allow the column to take all the remaining space */
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
th, td {
|
|
|
|
padding: 8px;
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
text-align: left;
|
|
|
|
}
|
|
|
|
|
|
|
|
th {
|
2024-10-24 11:17:00 +00:00
|
|
|
background-color: var(--header-background-color);
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.status-success {
|
|
|
|
color: green;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-fail {
|
|
|
|
color: red;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-pending {
|
|
|
|
color: #d4a017;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-broken {
|
|
|
|
color: purple;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-run {
|
|
|
|
color: blue;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-error {
|
|
|
|
color: darkred;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
|
|
|
|
.status-other {
|
|
|
|
color: grey;
|
|
|
|
font-weight: bold;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
</head>
|
|
|
|
<body>
|
2024-10-23 19:59:42 +00:00
|
|
|
<div id="info-container"></div>
|
|
|
|
<div id="status-container"></div>
|
|
|
|
<div id="result-container"></div>
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
<footer id="footer">
|
|
|
|
<div class="left"></div>
|
2024-10-23 19:59:42 +00:00
|
|
|
<div class="right"></div>
|
|
|
|
<div class="settings">
|
|
|
|
<span id="theme-toggle">☀️</span>
|
|
|
|
</div>
|
2024-10-01 19:19:35 +00:00
|
|
|
</footer>
|
|
|
|
|
|
|
|
<script>
|
2024-10-23 19:59:42 +00:00
|
|
|
function toggleTheme() {
|
|
|
|
document.body.classList.toggle('night-theme');
|
|
|
|
const toggleIcon = document.getElementById('theme-toggle');
|
|
|
|
if (document.body.classList.contains('night-theme')) {
|
|
|
|
toggleIcon.textContent = '☾'; // Moon for night mode
|
|
|
|
} else {
|
|
|
|
toggleIcon.textContent = '☀️'; // Sun for day mode
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-26 17:35:33 +00:00
|
|
|
function updateUrlParameter(paramName, paramValue) {
|
|
|
|
const url = new URL(window.location.href);
|
|
|
|
url.searchParams.set(paramName, paramValue);
|
|
|
|
window.location.href = url.toString();
|
|
|
|
}
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
// Attach the toggle function to the click event of the icon
|
|
|
|
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
function formatTimestamp(timestamp, showDate = true) {
|
|
|
|
const date = new Date(timestamp * 1000);
|
|
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
|
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
|
|
const month = monthNames[date.getMonth()];
|
2024-10-26 17:35:33 +00:00
|
|
|
//const year = date.getFullYear();
|
2024-10-01 19:19:35 +00:00
|
|
|
const hours = String(date.getHours()).padStart(2, '0');
|
|
|
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
2024-10-23 19:59:42 +00:00
|
|
|
//const milliseconds = String(date.getMilliseconds()).padStart(2, '0');
|
2024-10-01 19:19:35 +00:00
|
|
|
|
|
|
|
return showDate
|
2024-10-26 17:35:33 +00:00
|
|
|
? `${day}'${month} ${hours}:${minutes}:${seconds}`
|
2024-10-23 19:59:42 +00:00
|
|
|
: `${hours}:${minutes}:${seconds}`;
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
function formatDuration(durationInSeconds, detailed = false) {
|
2024-10-01 19:19:35 +00:00
|
|
|
// Check if the duration is empty, null, or not a number
|
|
|
|
if (!durationInSeconds || isNaN(durationInSeconds)) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure duration is a floating-point number
|
|
|
|
const duration = parseFloat(durationInSeconds);
|
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
if (detailed) {
|
|
|
|
// Format in the detailed format with hours, minutes, and seconds
|
|
|
|
const hours = Math.floor(duration / 3600);
|
|
|
|
const minutes = Math.floor((duration % 3600) / 60);
|
|
|
|
const seconds = Math.floor(duration % 60);
|
2024-10-01 19:19:35 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
const formattedHours = hours > 0 ? `${hours}h ` : '';
|
|
|
|
const formattedMinutes = minutes > 0 ? `${minutes}m ` : '';
|
|
|
|
const formattedSeconds = `${String(seconds).padStart(2, '0')}s`;
|
|
|
|
|
|
|
|
return `${formattedHours}${formattedMinutes}${formattedSeconds}`.trim();
|
|
|
|
} else {
|
|
|
|
// Format in the default format with seconds and milliseconds
|
|
|
|
const seconds = Math.floor(duration);
|
|
|
|
const milliseconds = Math.floor((duration % 1) * 1000);
|
|
|
|
|
|
|
|
const formattedSeconds = String(seconds);
|
2024-11-08 16:48:17 +00:00
|
|
|
const formattedMilliseconds = String(milliseconds).padStart(2, '0').slice(-2);
|
2024-10-24 11:17:00 +00:00
|
|
|
|
|
|
|
return `${formattedSeconds}.${formattedMilliseconds}`;
|
|
|
|
}
|
|
|
|
}
|
2024-10-01 19:19:35 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
// Function to determine status class based on value
|
|
|
|
function getStatusClass(status) {
|
|
|
|
const lowerStatus = status.toLowerCase();
|
|
|
|
if (lowerStatus.includes('success') || lowerStatus === 'ok') return 'status-success';
|
|
|
|
if (lowerStatus.includes('fail')) return 'status-fail';
|
|
|
|
if (lowerStatus.includes('pending')) return 'status-pending';
|
|
|
|
if (lowerStatus.includes('broken')) return 'status-broken';
|
|
|
|
if (lowerStatus.includes('run')) return 'status-run';
|
|
|
|
if (lowerStatus.includes('error')) return 'status-error';
|
|
|
|
return 'status-other';
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
2024-10-26 17:35:33 +00:00
|
|
|
function addKeyValueToStatus(key, value, options = null) {
|
2024-10-23 19:59:42 +00:00
|
|
|
const statusContainer = document.getElementById('status-container');
|
2024-10-01 19:19:35 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
let keyValuePair = document.createElement('div');
|
|
|
|
keyValuePair.className = 'key-value-pair';
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
const keyElement = document.createElement('div');
|
|
|
|
keyElement.className = 'json-key';
|
|
|
|
keyElement.textContent = key + ':';
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
let valueElement;
|
|
|
|
|
|
|
|
if (options) {
|
|
|
|
// Create dropdown if options are provided
|
2024-10-26 17:35:33 +00:00
|
|
|
valueElement = document.createElement('select');
|
|
|
|
valueElement.className = 'dropdown-value';
|
2024-10-29 21:09:03 +00:00
|
|
|
|
2024-10-26 17:35:33 +00:00
|
|
|
options.forEach(optionValue => {
|
|
|
|
const option = document.createElement('option');
|
|
|
|
option.value = optionValue;
|
2024-10-29 21:09:03 +00:00
|
|
|
option.textContent = optionValue.slice(0, 10);
|
|
|
|
|
|
|
|
// Set the initially selected option
|
|
|
|
if (optionValue === value) {
|
|
|
|
option.selected = true;
|
|
|
|
}
|
|
|
|
|
2024-10-26 17:35:33 +00:00
|
|
|
valueElement.appendChild(option);
|
|
|
|
});
|
2024-10-29 21:09:03 +00:00
|
|
|
|
|
|
|
// Update the URL parameter when the selected value changes
|
|
|
|
valueElement.addEventListener('change', (event) => {
|
|
|
|
const selectedValue = event.target.value;
|
|
|
|
updateUrlParameter(key, selectedValue);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// Create a simple text display if no options are provided
|
|
|
|
valueElement = document.createElement('div');
|
|
|
|
valueElement.className = 'json-value';
|
|
|
|
valueElement.textContent = value || 'N/A'; // Display 'N/A' if value is null
|
2024-10-26 17:35:33 +00:00
|
|
|
}
|
2024-10-29 21:09:03 +00:00
|
|
|
|
|
|
|
keyValuePair.appendChild(keyElement);
|
|
|
|
keyValuePair.appendChild(valueElement);
|
2024-10-24 11:17:00 +00:00
|
|
|
statusContainer.appendChild(keyValuePair);
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function addFileButtonToStatus(key, links) {
|
|
|
|
|
|
|
|
if (links == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const statusContainer = document.getElementById('status-container');
|
|
|
|
|
|
|
|
const keyElement = document.createElement('div');
|
|
|
|
keyElement.className = 'json-key';
|
2024-10-24 11:17:00 +00:00
|
|
|
keyElement.textContent = columnSymbols[key] + ':' || key;
|
2024-10-23 19:59:42 +00:00
|
|
|
statusContainer.appendChild(keyElement);
|
|
|
|
|
|
|
|
if (Array.isArray(links) && links.length > 0) {
|
|
|
|
links.forEach(link => {
|
2024-10-24 11:17:00 +00:00
|
|
|
const textLink = document.createElement('a');
|
|
|
|
textLink.href = link;
|
|
|
|
textLink.textContent = link.split('/').pop();
|
|
|
|
textLink.target = '_blank';
|
|
|
|
statusContainer.appendChild(textLink);
|
|
|
|
statusContainer.appendChild(document.createElement('br'));
|
2024-10-01 19:19:35 +00:00
|
|
|
});
|
|
|
|
}
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function addStatusToStatus(status, start_time, duration) {
|
2024-10-24 11:17:00 +00:00
|
|
|
const statusContainer = document.getElementById('status-container')
|
2024-10-01 19:19:35 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
let keyValuePair = document.createElement('div');
|
|
|
|
keyValuePair.className = 'key-value-pair';
|
2024-10-23 19:59:42 +00:00
|
|
|
let keyElement = document.createElement('div');
|
|
|
|
let valueElement = document.createElement('div');
|
|
|
|
keyElement.className = 'json-key';
|
|
|
|
valueElement.className = 'json-value';
|
2024-10-24 11:17:00 +00:00
|
|
|
keyElement.textContent = columnSymbols['status'] + ':' || 'status:';
|
2024-10-23 19:59:42 +00:00
|
|
|
valueElement.classList.add('status-value');
|
|
|
|
valueElement.classList.add(getStatusClass(status));
|
|
|
|
valueElement.textContent = status;
|
2024-10-24 11:17:00 +00:00
|
|
|
keyValuePair.appendChild(keyElement);
|
|
|
|
keyValuePair.appendChild(valueElement);
|
|
|
|
statusContainer.appendChild(keyValuePair);
|
2024-10-23 19:59:42 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
keyValuePair = document.createElement('div');
|
|
|
|
keyValuePair.className = 'key-value-pair';
|
2024-10-23 19:59:42 +00:00
|
|
|
keyElement = document.createElement('div');
|
|
|
|
valueElement = document.createElement('div');
|
|
|
|
keyElement.className = 'json-key';
|
|
|
|
valueElement.className = 'json-value';
|
2024-10-24 11:17:00 +00:00
|
|
|
keyElement.textContent = columnSymbols['start_time'] + ':' || 'start_time:';
|
2024-10-23 19:59:42 +00:00
|
|
|
valueElement.textContent = formatTimestamp(start_time);
|
2024-10-24 11:17:00 +00:00
|
|
|
keyValuePair.appendChild(keyElement);
|
|
|
|
keyValuePair.appendChild(valueElement);
|
|
|
|
statusContainer.appendChild(keyValuePair);
|
2024-10-23 19:59:42 +00:00
|
|
|
|
2024-10-24 11:17:00 +00:00
|
|
|
keyValuePair = document.createElement('div');
|
|
|
|
keyValuePair.className = 'key-value-pair';
|
2024-10-23 19:59:42 +00:00
|
|
|
keyElement = document.createElement('div');
|
|
|
|
valueElement = document.createElement('div');
|
|
|
|
keyElement.className = 'json-key';
|
|
|
|
valueElement.className = 'json-value';
|
2024-10-24 11:17:00 +00:00
|
|
|
keyElement.textContent = columnSymbols['duration'] + ':' || 'duration:';
|
2024-10-23 19:59:42 +00:00
|
|
|
if (duration === null) {
|
|
|
|
// Set initial value to 0 and add a unique ID or data attribute to identify the duration element
|
|
|
|
valueElement.textContent = '00:00:00';
|
|
|
|
valueElement.setAttribute('id', 'duration-value');
|
|
|
|
} else {
|
|
|
|
// Format the duration if it's a valid number
|
2024-10-24 11:17:00 +00:00
|
|
|
valueElement.textContent = formatDuration(duration, true);
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
2024-10-24 11:17:00 +00:00
|
|
|
keyValuePair.appendChild(keyElement);
|
|
|
|
keyValuePair.appendChild(valueElement);
|
|
|
|
statusContainer.appendChild(keyValuePair);
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function navigatePath(jsonObj, nameArray) {
|
|
|
|
let baseParams = new URLSearchParams(window.location.search);
|
|
|
|
let keysToDelete = [];
|
|
|
|
baseParams.forEach((value, key) => {
|
|
|
|
if (key.startsWith('name_')) {
|
|
|
|
keysToDelete.push(key); // Collect the keys to delete
|
|
|
|
}
|
|
|
|
});
|
|
|
|
keysToDelete.forEach((key) => baseParams.delete(key));
|
|
|
|
let pathNames = [];
|
|
|
|
let pathLinks = [];
|
|
|
|
let currentObj = jsonObj;
|
|
|
|
|
|
|
|
// Add the first entry (root level)
|
|
|
|
baseParams.set(`name_0`, currentObj.name);
|
|
|
|
pathNames.push(currentObj.name);
|
|
|
|
pathLinks.push(`<span class="separator">/</span><a href="${window.location.pathname}?${baseParams.toString()}">${currentObj.name}</a>`);
|
|
|
|
// Iterate through the nameArray starting at index 0
|
|
|
|
for (const [index, name] of nameArray.entries()) {
|
|
|
|
if (index === 0) continue;
|
|
|
|
if (currentObj && Array.isArray(currentObj.results)) {
|
|
|
|
const nextResult = currentObj.results.find(result => result.name === name);
|
|
|
|
if (nextResult) {
|
|
|
|
baseParams.set(`name_${index}`, nextResult.name);
|
|
|
|
pathNames.push(nextResult.name); // Correctly push nextResult name, not currentObj.name
|
|
|
|
pathLinks.push(`<span class="separator">/</span><a href="${window.location.pathname}?${baseParams.toString()}">${nextResult.name}</a>`);
|
|
|
|
currentObj = nextResult; // Move to the next object in the hierarchy
|
|
|
|
} else {
|
|
|
|
console.error(`Name "${name}" not found in results array.`);
|
|
|
|
return null; // Name not found in results array
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error(`Current object is not structured as expected.`);
|
|
|
|
return null; // Current object is not structured as expected
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const footerLeft = document.querySelector('#footer .left');
|
|
|
|
footerLeft.innerHTML = pathLinks.join('');
|
|
|
|
|
|
|
|
return currentObj;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Define the fixed columns globally, so both functions can use it
|
|
|
|
const columns = ['name', 'status', 'start_time', 'duration', 'info'];
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
const columnSymbols = {
|
2024-10-29 21:09:03 +00:00
|
|
|
name: '🗂️',
|
|
|
|
status: '🧾',
|
2024-10-23 19:59:42 +00:00
|
|
|
start_time: '🕒',
|
|
|
|
duration: '⏳',
|
2024-10-29 21:09:03 +00:00
|
|
|
info: '📝',
|
|
|
|
files: '📎'
|
2024-10-23 19:59:42 +00:00
|
|
|
};
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
function createResultsTable(results, nest_level) {
|
|
|
|
if (results && Array.isArray(results) && results.length > 0) {
|
|
|
|
const table = document.createElement('table');
|
|
|
|
const thead = document.createElement('thead');
|
|
|
|
const tbody = document.createElement('tbody');
|
|
|
|
|
|
|
|
// Create table headers based on the fixed columns
|
|
|
|
const headerRow = document.createElement('tr');
|
|
|
|
columns.forEach(column => {
|
|
|
|
const th = document.createElement('th');
|
2024-10-29 21:09:03 +00:00
|
|
|
th.textContent = columnSymbols[column] || column;
|
2024-10-01 19:19:35 +00:00
|
|
|
th.style.cursor = 'pointer'; // Make headers clickable
|
2024-10-29 21:09:03 +00:00
|
|
|
th.setAttribute('data-sort-direction', 'asc'); // Default sort direction
|
|
|
|
th.addEventListener('click', () => sortTable(results, column, columnSymbols[column] || column, tbody, nest_level, columns)); // Add click event to sort the table
|
2024-10-01 19:19:35 +00:00
|
|
|
headerRow.appendChild(th);
|
|
|
|
});
|
|
|
|
thead.appendChild(headerRow);
|
|
|
|
|
|
|
|
// Create table rows
|
|
|
|
populateTableRows(tbody, results, columns, nest_level);
|
|
|
|
|
|
|
|
table.appendChild(thead);
|
|
|
|
table.appendChild(tbody);
|
|
|
|
|
|
|
|
return table;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function populateTableRows(tbody, results, columns, nest_level) {
|
|
|
|
const currentUrl = new URL(window.location.href); // Get the current URL
|
|
|
|
|
|
|
|
// Clear existing rows if re-rendering (used in sorting)
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
|
|
|
|
results.forEach((result, index) => {
|
|
|
|
const row = document.createElement('tr');
|
|
|
|
|
|
|
|
columns.forEach(column => {
|
|
|
|
const td = document.createElement('td');
|
|
|
|
const value = result[column];
|
|
|
|
|
|
|
|
if (column === 'name') {
|
|
|
|
// Create a link for the name field, using name_X
|
|
|
|
const link = document.createElement('a');
|
|
|
|
const newUrl = new URL(currentUrl); // Create a fresh copy of the URL for each row
|
|
|
|
newUrl.searchParams.set(`name_${nest_level}`, value); // Use backticks for string interpolation
|
|
|
|
link.href = newUrl.toString();
|
|
|
|
link.textContent = value;
|
|
|
|
td.classList.add('name-column');
|
|
|
|
td.appendChild(link);
|
|
|
|
} else if (column === 'status') {
|
|
|
|
// Apply status formatting
|
|
|
|
const span = document.createElement('span');
|
|
|
|
span.className = getStatusClass(value);
|
|
|
|
span.textContent = value;
|
2024-10-23 19:59:42 +00:00
|
|
|
td.classList.add('status-column');
|
2024-10-01 19:19:35 +00:00
|
|
|
td.appendChild(span);
|
|
|
|
} else if (column === 'start_time') {
|
2024-10-23 19:59:42 +00:00
|
|
|
td.classList.add('time-column');
|
2024-10-01 19:19:35 +00:00
|
|
|
td.textContent = value ? formatTimestamp(value, false) : '';
|
|
|
|
} else if (column === 'duration') {
|
2024-10-23 19:59:42 +00:00
|
|
|
td.classList.add('time-column');
|
2024-10-01 19:19:35 +00:00
|
|
|
td.textContent = value ? formatDuration(value) : '';
|
|
|
|
} else if (column === 'info') {
|
2024-11-08 16:48:17 +00:00
|
|
|
td.textContent = value.includes('\n') ? '↵' : (value || '');
|
2024-10-01 19:19:35 +00:00
|
|
|
td.classList.add('info-column');
|
|
|
|
}
|
|
|
|
|
|
|
|
row.appendChild(td);
|
|
|
|
});
|
|
|
|
|
|
|
|
tbody.appendChild(row);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
function sortTable(results, column, key, tbody, nest_level, columns) {
|
2024-10-01 19:19:35 +00:00
|
|
|
// Find the table header element for the given key
|
2024-10-29 21:09:03 +00:00
|
|
|
const tableHeaders = document.querySelectorAll('th');
|
|
|
|
let th = Array.from(tableHeaders).find(header => header.textContent === key);
|
2024-10-01 19:19:35 +00:00
|
|
|
|
|
|
|
if (!th) {
|
|
|
|
console.error(`No table header found for key: ${key}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
const ascending = th.getAttribute('data-sort-direction') === 'asc';
|
|
|
|
th.setAttribute('data-sort-direction', ascending ? 'desc' : 'asc');
|
2024-10-01 19:19:35 +00:00
|
|
|
|
|
|
|
results.sort((a, b) => {
|
2024-10-29 21:09:03 +00:00
|
|
|
if (a[column] < b[column]) return ascending ? -1 : 1;
|
|
|
|
if (a[column] > b[column]) return ascending ? 1 : -1;
|
2024-10-01 19:19:35 +00:00
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
// Clear the existing rows in tbody
|
|
|
|
tbody.innerHTML = '';
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
// Re-populate the table with sorted data
|
|
|
|
populateTableRows(tbody, results, columns, nest_level);
|
|
|
|
}
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
function loadResultsJSON(PR, sha, nameParams) {
|
2024-10-23 19:59:42 +00:00
|
|
|
const infoElement = document.getElementById('info-container');
|
2024-10-01 19:19:35 +00:00
|
|
|
let lastModifiedTime = null;
|
|
|
|
const task = nameParams[0].toLowerCase();
|
|
|
|
|
|
|
|
// Construct the URL dynamically based on PR, sha, and name_X
|
|
|
|
const baseUrl = window.location.origin + window.location.pathname.replace('/json.html', '');
|
|
|
|
const path = `${baseUrl}/${encodeURIComponent(PR)}/${encodeURIComponent(sha)}/result_${task}.json`;
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
fetch(path, {cache: "no-cache"})
|
2024-10-01 19:19:35 +00:00
|
|
|
.then(response => {
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
}
|
|
|
|
lastModifiedTime = response.headers.get('Last-Modified');
|
|
|
|
return response.json();
|
|
|
|
})
|
|
|
|
.then(data => {
|
|
|
|
const linksDiv = document.getElementById('links');
|
2024-10-23 19:59:42 +00:00
|
|
|
const resultsDiv = document.getElementById('result-container');
|
2024-10-01 19:19:35 +00:00
|
|
|
const footerRight = document.querySelector('#footer .right');
|
|
|
|
|
|
|
|
let targetData = navigatePath(data, nameParams);
|
|
|
|
let nest_level = nameParams.length;
|
|
|
|
|
2024-11-05 12:59:14 +00:00
|
|
|
// Add footer links from top-level Result
|
|
|
|
if (Array.isArray(data.links) && data.links.length > 0) {
|
|
|
|
data.links.forEach(link => {
|
|
|
|
const a = document.createElement('a');
|
|
|
|
a.href = link;
|
|
|
|
a.textContent = link.split('/').pop();
|
|
|
|
a.target = '_blank';
|
|
|
|
footerRight.appendChild(a);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-10-01 19:19:35 +00:00
|
|
|
if (targetData) {
|
2024-11-08 16:48:17 +00:00
|
|
|
//infoElement.style.display = 'none';
|
|
|
|
infoElement.innerHTML = (targetData.info || '').replace(/\n/g, '<br>');
|
2024-10-01 19:19:35 +00:00
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
addStatusToStatus(targetData.status, targetData.start_time, targetData.duration)
|
|
|
|
|
|
|
|
// Handle links
|
|
|
|
addFileButtonToStatus('files', targetData.links)
|
2024-10-01 19:19:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
// Handle duration update if duration is null and start_time exists
|
2024-10-23 19:59:42 +00:00
|
|
|
if (targetData.duration === null && targetData.start_time) {
|
|
|
|
let duration = Math.floor(Date.now() / 1000 - targetData.start_time);
|
2024-10-01 19:19:35 +00:00
|
|
|
const durationElement = document.getElementById('duration-value');
|
|
|
|
|
|
|
|
const intervalId = setInterval(() => {
|
|
|
|
duration++;
|
2024-10-24 11:17:00 +00:00
|
|
|
durationElement.textContent = formatDuration(duration, true);
|
2024-10-01 19:19:35 +00:00
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If 'results' exists and is non-empty, create the table
|
2024-10-23 19:59:42 +00:00
|
|
|
const resultsData = targetData.results;
|
2024-10-01 19:19:35 +00:00
|
|
|
if (Array.isArray(resultsData) && resultsData.length > 0) {
|
|
|
|
const table = createResultsTable(resultsData, nest_level);
|
|
|
|
if (table) {
|
|
|
|
resultsDiv.appendChild(table);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2024-10-23 19:59:42 +00:00
|
|
|
infoElement.textContent = 'Object Not Found';
|
|
|
|
infoElement.style.display = 'block';
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set up auto-reload if Last-Modified header is present
|
|
|
|
if (lastModifiedTime) {
|
|
|
|
setInterval(() => {
|
|
|
|
checkForUpdate(path, lastModifiedTime);
|
|
|
|
}, 30000); // 30000 milliseconds = 30 seconds
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error('Error loading JSON:', error);
|
2024-10-23 19:59:42 +00:00
|
|
|
infoElement.textContent = 'Error loading data';
|
|
|
|
infoElement.style.display = 'block';
|
2024-10-01 19:19:35 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Function to check if the JSON file is updated
|
|
|
|
function checkForUpdate(path, lastModifiedTime) {
|
2024-10-23 19:59:42 +00:00
|
|
|
fetch(path, {method: 'HEAD'})
|
2024-10-01 19:19:35 +00:00
|
|
|
.then(response => {
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
}
|
|
|
|
const newLastModifiedTime = response.headers.get('Last-Modified');
|
|
|
|
if (newLastModifiedTime && new Date(newLastModifiedTime) > new Date(lastModifiedTime)) {
|
|
|
|
// If the JSON file has been updated, reload the page
|
|
|
|
window.location.reload();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error('Error checking for update:', error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the page and load JSON from URL parameter
|
|
|
|
function init() {
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
const PR = urlParams.get('PR');
|
|
|
|
const sha = urlParams.get('sha');
|
|
|
|
const root_name = urlParams.get('name_0');
|
|
|
|
const nameParams = [];
|
|
|
|
|
|
|
|
urlParams.forEach((value, key) => {
|
|
|
|
if (key.startsWith('name_')) {
|
|
|
|
const index = parseInt(key.split('_')[1], 10);
|
|
|
|
nameParams[index] = value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
let path_commits_json = '';
|
|
|
|
let commitsArray = [];
|
|
|
|
|
2024-10-23 19:59:42 +00:00
|
|
|
if (PR) {
|
2024-10-29 21:09:03 +00:00
|
|
|
addKeyValueToStatus("PR", PR);
|
|
|
|
const baseUrl = window.location.origin + window.location.pathname.replace('/json.html', '');
|
|
|
|
path_commits_json = `${baseUrl}/${encodeURIComponent(PR)}/commits.json`;
|
2024-10-23 19:59:42 +00:00
|
|
|
} else {
|
2024-10-29 21:09:03 +00:00
|
|
|
// Placeholder for a different path when PR is missing
|
|
|
|
console.error("PR parameter is missing. Setting alternate commits path.");
|
|
|
|
path_commits_json = '/path/to/alternative/commits.json';
|
2024-10-23 19:59:42 +00:00
|
|
|
}
|
|
|
|
|
2024-10-29 21:09:03 +00:00
|
|
|
function loadCommitsArray(path) {
|
|
|
|
return fetch(path, { cache: "no-cache" })
|
|
|
|
.then(response => {
|
|
|
|
if (!response.ok) {
|
|
|
|
console.error(`HTTP error! status: ${response.status}`)
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return response.json();
|
|
|
|
})
|
|
|
|
.then(data => {
|
|
|
|
if (Array.isArray(data) && data.every(item => typeof item === 'object' && item.hasOwnProperty('sha'))) {
|
|
|
|
return data.map(item => item.sha);
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid data format: expected array of objects with a "sha" key');
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error('Error loading commits JSON:', error);
|
|
|
|
return []; // Return an empty array if an error occurs
|
|
|
|
});
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
2024-10-29 21:09:03 +00:00
|
|
|
|
|
|
|
loadCommitsArray(path_commits_json)
|
|
|
|
.then(data => {
|
|
|
|
commitsArray = data;
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
// Proceed with the rest of the initialization
|
|
|
|
addKeyValueToStatus("sha", sha || "latest", commitsArray.concat(["latest"]));
|
|
|
|
|
|
|
|
if (nameParams[1]) {
|
|
|
|
addKeyValueToStatus("job", nameParams[1]);
|
|
|
|
}
|
|
|
|
addKeyValueToStatus("workflow", nameParams[0]);
|
|
|
|
|
|
|
|
// Check if all required parameters are present to load JSON
|
|
|
|
if (PR && sha && root_name) {
|
2024-11-08 16:48:17 +00:00
|
|
|
const shaToLoad = (sha === 'latest') ? commitsArray[commitsArray.length - 1] : sha;
|
|
|
|
loadResultsJSON(PR, shaToLoad, nameParams);
|
2024-10-29 21:09:03 +00:00
|
|
|
} else {
|
|
|
|
document.getElementById('title').textContent = 'Error: Missing required URL parameters: PR, sha, or name_0';
|
|
|
|
}
|
|
|
|
});
|
2024-10-01 19:19:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
window.onload = init;
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|