Skip to content

Instantly share code, notes, and snippets.

@DeflateAwning
Last active August 6, 2024 17:38
Show Gist options
  • Save DeflateAwning/d8d42a082cb27b7d01df751d0dc26f31 to your computer and use it in GitHub Desktop.
Save DeflateAwning/d8d42a082cb27b7d01df751d0dc26f31 to your computer and use it in GitHub Desktop.
Tampermonkey script to modify the AWS Batch run page to add an execution time field to the "Job attempts" tab
// ==UserScript==
// @name AWS Batch Upgrades
// @namespace https://gist.github.com/DeflateAwning/d8d42a082cb27b7d01df751d0dc26f31
// @version 0.2.5
// @description Calculate AWS Batch job time since started and total execution time (on the "Job attempts" tab). Set the tab title to include the job name and execution status. Fix the bug where you logout and lose the job you had open.
// @author DeflateAwning
// @match https://*.console.aws.amazon.com/batch/home?*
// @grant none
// ==/UserScript==
// Instructions to Install:
// 1. On the GitHub Gist (link above), click the "Raw" button.
// 2. Tamper Monkey will prompt you to install the "user script". Click Install.
(function() {
'use strict';
const PRERUN_STATUS_VALUES = ['Runnable', 'Submitted', 'Starting', 'Ready'];
function calculateTimeDifference(startedAt, stoppedAt) {
if (!startedAt || !stoppedAt) {
return "N/A";
}
const timeDifference = stoppedAt.getTime() - startedAt.getTime();
const hours = Math.floor(timeDifference / (1000 * 60 * 60));
const minutes = Math.floor((timeDifference % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeDifference % (1000 * 60)) / 1000);
return `${hours}h ${minutes}m ${seconds}s`;
}
// Function to create and append the result field.
function appendExecutionTimeField() {
// Get the "Started at" and "Stopped at" elements.
const startedAtElement = document.querySelector('[data-test-id="startedAt"]').querySelector('div:last-child');
const stoppedAtElement = document.querySelector('[data-test-id="stoppedAt"]').querySelector('div:last-child');
// Extract the text content from the elements.
const startedAtText = startedAtElement.textContent.trim();
const stoppedAtText = stoppedAtElement.textContent.trim();
// Create date objects.
const startedAtDate = new Date(startedAtText);
const stoppedAtDate = new Date(stoppedAtText);
const timeSinceStarted = calculateTimeDifference(startedAtDate, new Date());
const totalExecutionTime = calculateTimeDifference(startedAtDate, stoppedAtDate);
// Remove existing "tamper-time-summary" objects.
document.querySelectorAll('.tamper-time-summary').forEach(e => e.remove());
const newElementHTML = `
<div style="color: rgb(139,0,0);" class="tamper-time-summary">
<div>
<span style="color: #545b64;">Time Since Start:</span>
${timeSinceStarted}
</div>
<div>
<span style="color: #545b64;">Total Execution Time:</span>
${totalExecutionTime}
</div>
<div style="color: #545b64;">Note: Times are in local time.</div>
</div>
`;
// Find the target element by its data-test-id.
const targetElement = document.querySelector('[data-test-id="stoppedAt"]');
// Append newElementHTML right after targetElement.
targetElement.insertAdjacentHTML('afterend', newElementHTML);
}
function getJobName() {
// let jobNameElement = document.evaluate('//*[@id="heading:rm:"]', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
let jobNameElement = document.evaluate('//*[@id="app"]/div/div/div/main/div/div[2]/div/div/nav/ol/li[4]/div/span/span', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (jobNameElement) {
let jobName = jobNameElement.textContent.trim();
return jobName;
}
}
function getJobStatus() {
let jobStatusElement = document.evaluate("//*[contains(@class, 'awsui_content-wrapper')]/div[2]/div/div/div/div/div[1]/div/div/div[2]/span/span/text()", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (jobStatusElement) {
let jobStatus = jobStatusElement.textContent.trim();
return jobStatus;
}
}
function checkAndUpdatePageTitle() {
var currentUrl = window.location.href;
// Check if the URL ends with "#jobs" and contains "/batch/home"
if (currentUrl.endsWith("#jobs") && currentUrl.includes("/batch/home")) {
// Set the page title
document.title = "JOB LIST | AWS Batch";
// console.log("Set job title.");
}
else if (currentUrl.includes("/batch/home") && currentUrl.includes("#jobs/fargate/detail/")) {
let job_name = getJobName();
let job_status = getJobStatus();
let new_title = '';
if (job_status == 'Running') {
new_title += '🕙';
}
else if (job_status == 'Success' || job_status == 'Succeeded') {
new_title += '✅';
}
else if (job_status == 'Failed') {
new_title += '❌';
}
else if (PRERUN_STATUS_VALUES.includes(job_status)) { // like 'Starting'
new_title += '🏁';
}
else if (job_status) {
new_title += '🤷'; // non-null but unknown
}
else {
new_title += '🫙'; // null jar
}
new_title += ' | ';
if (job_name) {
new_title += job_name;
}
else {
new_title += 'JOB';
//console.log("getJobName() returned empty job name.");
}
new_title += ' | BATCH';
document.title = new_title;
}
}
function getAwsRegionFromCurrentUrl() {
// Returns the region (like "us-east-1") as a string.
// If there are issues, consider using this method instead: https://gist.github.com/rams3sh/4858d5150acba5383dd697fda54dda2c
const regionMatch = window.location.href.match(/([a-z]{1,5}-[a-z]{2,20}-\d+)\.console\.aws\.amazon\.com/);
const region = regionMatch ? regionMatch[1] : null;
return region;
}
function fixBadLoginOnJobPage() {
// Fix the bug where you logout and lose the job you had open.
// Skip this part if we're not on that error page.
// Return if the page title isn't "Unauthorized".
if (document.title != "Unauthorized") {
return;
}
// Return if the URL isn't >1000 chars.
if (window.location.href.length <= 1000) {
return;
}
// Get the job ID from the URL. Regex extract the only UUID4.
// Example partial URL: https://us-east-1.console.aws.amazon.com/batch/home?hashArgs=%23jobs%2Ffargate%2Fdetail%2F90d176de-1550-4b94-8013-cb9c077a61cc&isauthcode=true
// Extract the first matching group.
const job_id_match = window.location.href.match(/batch.+hashArgs.+detail%2[Ff]([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})/);
const job_id = job_id_match ? job_id_match[1] : null;
let awsRegion = getAwsRegionFromCurrentUrl();
// Step 1: Find the <h1> tag
const h1Tag = document.querySelector('h1');
// Step 2: Create a new <div> element
const newDiv = document.createElement('div');
const jobLinkUrl = `https://${awsRegion}.console.aws.amazon.com/batch/home?region=${awsRegion}#jobs/fargate/detail/${job_id}`;
newDiv.innerHTML = `
<p>
<strong>Message from TamperMonkey script:</strong>
AWS did the bug thing where you can't easily see the job you had open.
First, <a href="https://${awsRegion}.console.aws.amazon.com/batch/home?region=${awsRegion}#jobs" target="_blank">sign in in a new tab</a>,
then use this link to <a href="${jobLinkUrl}" target="_blank">go back to the job you had open (${job_id}).</a>
<hr />
<strong>Message from TamperMonkey script:</strong> If I were you, I wouldn't ever click the "sign in again" link below.
</p>
`;
// Step 5: Insert the <div> element after the <h1> tag
h1Tag.insertAdjacentElement('afterend', newDiv);
}
function run_ignore_error() {
try {
appendExecutionTimeField();
} catch (error) {
console.error('appendExecutionTimeField: An error occurred:', error);
}
try {
checkAndUpdatePageTitle();
} catch (error) {
console.error('checkAndUpdatePageTitle: An error occurred:', error);
}
}
// Run every few seconds to keep time updated.
setInterval(run_ignore_error, 2000);
// Auto-reload the page if it says the job is Running.
setInterval(
function() {
let job_status = getJobStatus();
if (job_status == 'Running' || PRERUN_STATUS_VALUES.includes(job_status)) {
location.reload();
}
},
60 * 4 * 1000 // 4 minutes
);
// Run once at start, otherwise too many messages appear.
try {
fixBadLoginOnJobPage();
} catch (error) {
console.error('fixBadLoginOnJobPage: An error occurred:', error);
}
// Debug run.
// appendExecutionTimeField();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment