From e5604a8f12ede03836058655ac603393a0e5b080 Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Mon, 9 Sep 2024 13:13:32 +0100 Subject: [PATCH 1/7] Added XDMoD analytics metrics to jobs widget --- .../app/assets/stylesheets/application.scss | 1 + .../app/assets/stylesheets/xdmod.scss | 20 +++++ apps/dashboard/app/javascript/xdmod.js | 65 ++++++++++++---- apps/dashboard/app/javascript/xdmod/jobs.js | 78 +++++++++++++++++-- .../views/widgets/_xdmod_widget_jobs.html.erb | 55 +------------ 5 files changed, 141 insertions(+), 78 deletions(-) create mode 100644 apps/dashboard/app/assets/stylesheets/xdmod.scss diff --git a/apps/dashboard/app/assets/stylesheets/application.scss b/apps/dashboard/app/assets/stylesheets/application.scss index e36d0543d..6b715bc32 100644 --- a/apps/dashboard/app/assets/stylesheets/application.scss +++ b/apps/dashboard/app/assets/stylesheets/application.scss @@ -173,6 +173,7 @@ small.form-text { @import "editor"; @import "icon_picker"; @import "pinned_apps"; +@import "xdmod"; @import "support_ticket"; @import "data_tables"; @import "projects"; diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss new file mode 100644 index 000000000..2fb514b87 --- /dev/null +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -0,0 +1,20 @@ +/** Job Analytics **/ +#jobsPanelDiv { + .hiddenRow { + padding: 0 !important; + } + + i.app-icon { + width: 0.9rem; + height: 0.9rem; + font-size: 0.9rem; + } + + tr[aria-expanded=true] .closed { + display: none; + } + + tr[aria-expanded=false] .open { + display: none; + } +} \ No newline at end of file diff --git a/apps/dashboard/app/javascript/xdmod.js b/apps/dashboard/app/javascript/xdmod.js index 01371a4a0..d7658750c 100644 --- a/apps/dashboard/app/javascript/xdmod.js +++ b/apps/dashboard/app/javascript/xdmod.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import {xdmodUrl, analyticsPath} from './config'; import {today, startOfYear, thirtyDaysAgo} from './utils'; -import { jobsPanel } from './xdmod/jobs'; +import { jobsPanel, jobAnalyticsTable } from './xdmod/jobs'; import Handlebars from 'handlebars'; const jobsPageLimit = 10; @@ -45,18 +45,18 @@ const jobHelpers = { return `${month}/${day}`; }, job_url: function(id){ return `${xdmodUrl()}/#job_viewer?action=show&realm=SUPREMM&jobref=${id}`; }, - cpu_label: function(cpu){ - let value = (parseFloat(cpu)*100).toFixed(1), - label = "N/A"; + efficiency_label: function(efficiencyValue, inverse = false){ + const value = (parseFloat(efficiencyValue)*100).toFixed(1); + let label = "N/A"; if(! isNaN(value)){ let severity = "warning"; - if(cpu > 0.74){ - severity = "success"; + if(efficiencyValue > 0.74){ + severity = inverse ? "danger" : "success"; } - else if(cpu < 0.25){ - severity = "danger"; + else if(efficiencyValue < 0.25){ + severity = inverse ? "success" : "danger"; } label = `${Handlebars.escapeExpression(value.toString().padStart(4,0))}`; @@ -177,6 +177,14 @@ function jobsUrl(user){ return url; } +function jobAnalyticsUrl(jobId){ + let url = new URL(`${xdmodUrl()}/rest/v1.0/warehouse/search/jobs/analytics`); + url.searchParams.set('_dc', Date.now()); + url.searchParams.set('realm', 'SUPREMM'); + url.searchParams.set('jobid', jobId); + return url; +} + function aggregateDataUrl(user){ var url = new URL(`${xdmodUrl()}/rest/v1/warehouse/aggregatedata`); url.searchParams.set('_dc', Date.now()); @@ -210,6 +218,11 @@ function renderJobs(context) { panel.replaceChildren(jobs); } +function renderJobAnalytics(jobAnalytics, containerId) { + const analyticsTable = jobAnalyticsTable(jobAnalytics, jobHelpers) + $(containerId).html(analyticsTable); +} + function renderJobsEfficiency(context) { const newConext = _.merge(context, {unit: "jobs", unit_title: "Jobs"}); const templateSource = $('#job-efficiency-template').html(); @@ -239,13 +252,24 @@ function createJobsWidget() { console.error(error); renderJobs({error: error}); - // error - report back for analytics purposes - const analyticsUrl = new URL(analyticsPath('xdmod_jobs_widget_error'), document.location); - analyticsUrl.searchParams.append('error', error); - fetch(analyticsUrl); + reportError('xdmod_jobs_widget_error', error); }); } +function addAnalyticsToJob(jobId) { + const analyticsContainer = `#details_${jobId}`; + fetch(jobAnalyticsUrl(jobId), { credentials: 'include' }) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then((data) => renderJobAnalytics(data, analyticsContainer)) + .catch((error) => { + console.error(error); + renderJobAnalytics({error: error}, analyticsContainer); + + reportError('xdmod_jobs_analytics_widget_error', error); + }); +} + function createEfficiencyWidgets() { const jobPanel = $(`#${jobEfficiencyPanelId}`); const corePanel = $(`#${coreEfficiencyPanelId}`); @@ -287,17 +311,26 @@ function createEfficiencyWidgets() { renderJobsEfficiency({error: error}); renderCoreHoursEfficiency({error: error}); - // error - report back for analytics purposes - const analyticsUrl = new URL(analyticsPath('xdmod_jobs_widget_error'), document.location); - analyticsUrl.searchParams.append('error', error); - fetch(analyticsUrl); + reportError('xdmod_jobs_widget_error', error); }); } +function reportError(path, error) { + // error - report back for analytics purposes + const analyticsUrl = new URL(analyticsPath(path), document.location); + analyticsUrl.searchParams.append('error', error); + fetch(analyticsUrl); +} + jQuery(() => { createJobsWidget(); createEfficiencyWidgets(); // initialize the panels renderJobs({ loading: true }); + + $('#jobsPanelDiv').on('click', 'tr[data-xdmod-jobid][aria-expanded="true"]', function(event) { + const jobId = event.currentTarget.getAttribute("data-xdmod-jobid"); + addAnalyticsToJob(jobId) + }); }); \ No newline at end of file diff --git a/apps/dashboard/app/javascript/xdmod/jobs.js b/apps/dashboard/app/javascript/xdmod/jobs.js index ac32aa4ee..50f75ecde 100644 --- a/apps/dashboard/app/javascript/xdmod/jobs.js +++ b/apps/dashboard/app/javascript/xdmod/jobs.js @@ -9,6 +9,24 @@ export function jobsPanel(context, helpers){ return div; } +export function jobAnalyticsTable(context, jobHelpers) { + if(context.error !== undefined) { + return errorBody(context.error, jobHelpers); + } + + const dataByKey = context.data.reduce((acc, obj) => { + acc[obj.key] = obj; + return acc; + }, {}); + const cpuEfficiency = jobHelpers.efficiency_label(dataByKey['CPU User']?.value, false) + const memEfficiency = jobHelpers.efficiency_label(dataByKey['Memory Headroom']?.value, true) + const walltimeEfficiency = jobHelpers.efficiency_label(dataByKey['Walltime Accuracy']?.value, false) + const analyticsContent = `${cpuEfficiency} + ${memEfficiency} + ${walltimeEfficiency}`; + return analyticsTable(analyticsContent); +} + function card(context, helpers) { const div = document.createElement('div'); div.classList.add('card', 'mt-3'); @@ -77,15 +95,15 @@ function table(context, helpers) { // tableElement.classList.add('table', 'table-sm', 'table-striped', 'table-condensed'); - thead = document.createElement('thead'); + const thead = document.createElement('thead'); thead.innerHTML = ' \ + \ \ \ \ - \ '; - tbody = document.createElement('tbody'); + const tbody = document.createElement('tbody'); tbody.append(...tableRows(context, helpers)); tableElement.append(thead); @@ -97,12 +115,12 @@ function table(context, helpers) { } function tableRows(context, helpers) { - jobs = context.results; + const jobs = context.results; if (jobs === undefined || jobs.length == 0) { return [ noDataRow() ]; } - rows = []; + const rows = []; // // @@ -113,6 +131,19 @@ function tableRows(context, helpers) { jobs.forEach(job => { const tr = document.createElement('tr'); tr.title = `${job.job_name} - ${job.local_job_id}`; + // Job Analytics metadata => Required for the AJAX request and collapse behaviour + tr.setAttribute('data-xdmod-jobid', job.jobid); + tr.setAttribute('data-bs-toggle', 'collapse'); + tr.setAttribute('data-bs-target', `#details_${job.jobid}`); + tr.setAttribute('aria-expanded', 'false'); + + // Job analytics collapse icons + const td0 = document.createElement('td'); + td0.innerHTML = ` + ` // - const td4 = document.createElement('td'); - td4.innerHTML = helpers.cpu_label(job.cpu_user); + // Not used with new analytics data + // const td4 = document.createElement('td'); + // td4.innerHTML = helpers.efficiency_label(job.cpu_user); - tr.append(td1, td2, td3, td4); + tr.append(td0, td1, td2, td3); rows.push(tr); + + // Add job analytics placeholder + const analyticsRow = document.createElement('tr'); + const analyticsData = analyticsTable('') + analyticsRow.innerHTML = ` + `; + rows.push(analyticsRow); }); return rows; } +function analyticsTable(analyticsContent) { + const analyticsTable = ` +
IDNameDateCPU
{{local_job_id}}  // {{local_job_id}}  @@ -132,17 +163,48 @@ function tableRows(context, helpers) { td3.innerText = helpers.date(job); // {{cpu_label cpu_user}}LOADING... +
+ ${analyticsData} +
+
+ + + + + + + + + + ${analyticsContent} + + +
CPUMemWalltime
`; + return analyticsTable; +} + function jobLink(url, id){ const a = document.createElement('a'); a.href = url; diff --git a/apps/dashboard/app/views/widgets/_xdmod_widget_jobs.html.erb b/apps/dashboard/app/views/widgets/_xdmod_widget_jobs.html.erb index 3e91f6e2b..6dc454e34 100644 --- a/apps/dashboard/app/views/widgets/_xdmod_widget_jobs.html.erb +++ b/apps/dashboard/app/views/widgets/_xdmod_widget_jobs.html.erb @@ -1,59 +1,6 @@ <%= javascript_include_tag 'xdmod', nonce: true %>
+ <%# This XDMoD widget is rendered using Javascript %>
- - -
From 80453a6b032d6b9d15b1a7f4f801ecd4a91af389 Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Wed, 11 Sep 2024 17:22:21 +0100 Subject: [PATCH 2/7] Updated XDMoD analytics rendering to remove nested tables --- .../app/assets/stylesheets/xdmod.scss | 13 +++++++ apps/dashboard/app/javascript/xdmod.js | 16 ++++---- apps/dashboard/app/javascript/xdmod/jobs.js | 37 ++++++------------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss index 2fb514b87..8b538077d 100644 --- a/apps/dashboard/app/assets/stylesheets/xdmod.scss +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -17,4 +17,17 @@ tr[aria-expanded=false] .open { display: none; } + + .job-analytics { + padding: 0.50rem 0.25rem; + + span { + padding-left: 8px; + padding-right: 8px; + } + + strong { + font-weight: 600; + } + } } \ No newline at end of file diff --git a/apps/dashboard/app/javascript/xdmod.js b/apps/dashboard/app/javascript/xdmod.js index d7658750c..29ec450be 100644 --- a/apps/dashboard/app/javascript/xdmod.js +++ b/apps/dashboard/app/javascript/xdmod.js @@ -2,12 +2,13 @@ import _ from 'lodash'; import {xdmodUrl, analyticsPath} from './config'; import {today, startOfYear, thirtyDaysAgo} from './utils'; -import { jobsPanel, jobAnalyticsTable } from './xdmod/jobs'; +import { jobsPanel, jobAnalyticsHtml } from './xdmod/jobs'; import Handlebars from 'handlebars'; const jobsPageLimit = 10; const jobHelpers = { + realm: 'Jobs', title: function(){ return "Recently Completed Jobs"; }, @@ -44,7 +45,7 @@ const jobHelpers = { return `${month}/${day}`; }, - job_url: function(id){ return `${xdmodUrl()}/#job_viewer?action=show&realm=SUPREMM&jobref=${id}`; }, + job_url: function(id){ return `${xdmodUrl()}/#job_viewer?action=show&realm=${this.realm}&jobref=${id}`; }, efficiency_label: function(efficiencyValue, inverse = false){ const value = (parseFloat(efficiencyValue)*100).toFixed(1); let label = "N/A"; @@ -153,6 +154,7 @@ var promiseLoggedIntoXDMoD = (function(){ }) .then((user_data) => { if(user_data && user_data.success && user_data.results && user_data.results.person_id){ + jobHelpers.realm = user_data.results.raw_data_allowed_realms?.includes('SUPREMM') ? 'SUPREMM' : 'Jobs'; return Promise.resolve(user_data); } else{ @@ -169,7 +171,7 @@ function jobsUrl(user){ url.searchParams.set('_dc', Date.now()); url.searchParams.set('start_date', thirtyDaysAgo()); url.searchParams.set('end_date', today()); - url.searchParams.set('realm', user?.results?.raw_data_allowed_realms?.includes('SUPREMM') ? 'SUPREMM' : 'Jobs'); + url.searchParams.set('realm', jobHelpers.realm); url.searchParams.set('limit', jobsPageLimit); url.searchParams.set('start', 0); url.searchParams.set('verbose', true); @@ -180,7 +182,7 @@ function jobsUrl(user){ function jobAnalyticsUrl(jobId){ let url = new URL(`${xdmodUrl()}/rest/v1.0/warehouse/search/jobs/analytics`); url.searchParams.set('_dc', Date.now()); - url.searchParams.set('realm', 'SUPREMM'); + url.searchParams.set('realm', jobHelpers.realm); url.searchParams.set('jobid', jobId); return url; } @@ -219,8 +221,8 @@ function renderJobs(context) { } function renderJobAnalytics(jobAnalytics, containerId) { - const analyticsTable = jobAnalyticsTable(jobAnalytics, jobHelpers) - $(containerId).html(analyticsTable); + const analyticsHtml = jobAnalyticsHtml(jobAnalytics, jobHelpers) + $(containerId).html(analyticsHtml); } function renderJobsEfficiency(context) { @@ -329,7 +331,7 @@ jQuery(() => { // initialize the panels renderJobs({ loading: true }); - $('#jobsPanelDiv').on('click', 'tr[data-xdmod-jobid][aria-expanded="true"]', function(event) { + $(`#${jobPanelId}`).on('click', 'tr[data-xdmod-jobid][aria-expanded="true"]', function(event) { const jobId = event.currentTarget.getAttribute("data-xdmod-jobid"); addAnalyticsToJob(jobId) }); diff --git a/apps/dashboard/app/javascript/xdmod/jobs.js b/apps/dashboard/app/javascript/xdmod/jobs.js index 50f75ecde..6c3cc1cb7 100644 --- a/apps/dashboard/app/javascript/xdmod/jobs.js +++ b/apps/dashboard/app/javascript/xdmod/jobs.js @@ -9,7 +9,7 @@ export function jobsPanel(context, helpers){ return div; } -export function jobAnalyticsTable(context, jobHelpers) { +export function jobAnalyticsHtml(context, jobHelpers) { if(context.error !== undefined) { return errorBody(context.error, jobHelpers); } @@ -21,10 +21,12 @@ export function jobAnalyticsTable(context, jobHelpers) { const cpuEfficiency = jobHelpers.efficiency_label(dataByKey['CPU User']?.value, false) const memEfficiency = jobHelpers.efficiency_label(dataByKey['Memory Headroom']?.value, true) const walltimeEfficiency = jobHelpers.efficiency_label(dataByKey['Walltime Accuracy']?.value, false) - const analyticsContent = `${cpuEfficiency} - ${memEfficiency} - ${walltimeEfficiency}`; - return analyticsTable(analyticsContent); + const analyticsContent = `
+ CPU: ${cpuEfficiency} + Mem: ${memEfficiency} + Walltime: ${walltimeEfficiency} +
`; + return analyticsContent } function card(context, helpers) { @@ -96,6 +98,7 @@ function table(context, helpers) { tableElement.classList.add('table', 'table-sm', 'table-striped', 'table-condensed'); const thead = document.createElement('thead'); + // Empty th to accommodate for the job analytics button thead.innerHTML = ' \ \ ID \ @@ -173,11 +176,12 @@ function tableRows(context, helpers) { // Add job analytics placeholder const analyticsRow = document.createElement('tr'); - const analyticsData = analyticsTable('LOADING...') analyticsRow.innerHTML = `
- ${analyticsData} +
+ LOADING... +
`; rows.push(analyticsRow); @@ -186,25 +190,6 @@ function tableRows(context, helpers) { return rows; } -function analyticsTable(analyticsContent) { - const analyticsTable = ` - - - - - - - - - - - ${analyticsContent} - - -
CPUMemWalltime
`; - return analyticsTable; -} - function jobLink(url, id){ const a = document.createElement('a'); a.href = url; From d82131e85fb25d45a711406304a0e102da0acbdc Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Wed, 11 Sep 2024 17:59:29 +0100 Subject: [PATCH 3/7] Improved XDMoD job analytics padding --- apps/dashboard/app/assets/stylesheets/xdmod.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss index 8b538077d..3035308a0 100644 --- a/apps/dashboard/app/assets/stylesheets/xdmod.scss +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -19,12 +19,9 @@ } .job-analytics { - padding: 0.50rem 0.25rem; - - span { - padding-left: 8px; - padding-right: 8px; - } + display: flex; + justify-content: space-between; + padding: 0.50rem 0.5rem; strong { font-weight: 600; From bf7bb3ad4f32dcc0344bdb9471b7414cb865b0c1 Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Thu, 12 Sep 2024 13:59:37 +0100 Subject: [PATCH 4/7] Fixed XDMoD Javascript login methods and error handling --- apps/dashboard/app/javascript/xdmod.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/app/javascript/xdmod.js b/apps/dashboard/app/javascript/xdmod.js index 29ec450be..b219b3d8a 100644 --- a/apps/dashboard/app/javascript/xdmod.js +++ b/apps/dashboard/app/javascript/xdmod.js @@ -85,12 +85,12 @@ var efficiencyHelpers = { } }; -function promiseLoginToXDMoD(xdmodUrl){ +function promiseLoginToXDMoD(){ return new Promise(function(resolve, reject){ var promise_to_receive_message_from_iframe = new Promise(function(resolve, reject){ window.addEventListener("message", function(event){ - if (event.origin !== xdmodUrl){ + if (event.origin !== xdmodUrl()){ console.log('Received message from untrusted origin, discarding'); return; } @@ -107,8 +107,8 @@ function promiseLoginToXDMoD(xdmodUrl){ }, false); }); - fetch(xdmodUrl + '/rest/auth/idpredirect?returnTo=%2Fgui%2Fgeneral%2Flogin.php') - .then(response => response.ok ? Promise.resolve(response) : Promise.reject()) + fetch(xdmodUrl() + '/rest/auth/idpredirect?returnTo=%2Fgui%2Fgeneral%2Flogin.php') + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Login failed: IDP redirect failed'))) .then(response => response.json()) .then(function(data){ return new Promise(function(resolve, reject){ @@ -280,7 +280,7 @@ function createEfficiencyWidgets() { return; } - promiseLoggedIntoXDMoD(xdmodUrl) + promiseLoggedIntoXDMoD() .then((user_data) => fetch(aggregateDataUrl(user_data), { credentials: 'include' })) .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) .then(response => response.json()) From a270a611868a4d4f06476e2de20ef839e5e9faa5 Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Fri, 13 Sep 2024 10:57:13 +0100 Subject: [PATCH 5/7] Moved all XDMoD analytics code into xdmod/jobs.js file --- .../app/assets/stylesheets/xdmod.scss | 2 +- apps/dashboard/app/javascript/utils.js | 9 +++ apps/dashboard/app/javascript/xdmod.js | 35 ++------- apps/dashboard/app/javascript/xdmod/jobs.js | 72 +++++++++++++------ 4 files changed, 67 insertions(+), 51 deletions(-) diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss index 3035308a0..d1b2249f5 100644 --- a/apps/dashboard/app/assets/stylesheets/xdmod.scss +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -21,7 +21,7 @@ .job-analytics { display: flex; justify-content: space-between; - padding: 0.50rem 0.5rem; + padding: 0.75rem 0.5rem; strong { font-weight: 600; diff --git a/apps/dashboard/app/javascript/utils.js b/apps/dashboard/app/javascript/utils.js index d855275eb..bd9d29adc 100644 --- a/apps/dashboard/app/javascript/utils.js +++ b/apps/dashboard/app/javascript/utils.js @@ -1,3 +1,4 @@ +import {analyticsPath} from "./config"; export function cssBadgeForState(state){ switch (state) { @@ -80,3 +81,11 @@ export function openLinkInJs(event) { mainDiv.prepend(alertDiv); } } + +export function reportErrorForAnalytics(path, error) { + // error - report back for analytics purposes + const analyticsUrl = new URL(analyticsPath(path), document.location); + analyticsUrl.searchParams.append('error', error); + // Fire and Forget + fetch(analyticsUrl); +} diff --git a/apps/dashboard/app/javascript/xdmod.js b/apps/dashboard/app/javascript/xdmod.js index b219b3d8a..41ca212dd 100644 --- a/apps/dashboard/app/javascript/xdmod.js +++ b/apps/dashboard/app/javascript/xdmod.js @@ -1,8 +1,8 @@ import _ from 'lodash'; import {xdmodUrl, analyticsPath} from './config'; -import {today, startOfYear, thirtyDaysAgo} from './utils'; -import { jobsPanel, jobAnalyticsHtml } from './xdmod/jobs'; +import {today, startOfYear, thirtyDaysAgo, reportErrorForAnalytics} from './utils'; +import { jobsPanel } from './xdmod/jobs'; import Handlebars from 'handlebars'; const jobsPageLimit = 10; @@ -179,14 +179,6 @@ function jobsUrl(user){ return url; } -function jobAnalyticsUrl(jobId){ - let url = new URL(`${xdmodUrl()}/rest/v1.0/warehouse/search/jobs/analytics`); - url.searchParams.set('_dc', Date.now()); - url.searchParams.set('realm', jobHelpers.realm); - url.searchParams.set('jobid', jobId); - return url; -} - function aggregateDataUrl(user){ var url = new URL(`${xdmodUrl()}/rest/v1/warehouse/aggregatedata`); url.searchParams.set('_dc', Date.now()); @@ -220,11 +212,6 @@ function renderJobs(context) { panel.replaceChildren(jobs); } -function renderJobAnalytics(jobAnalytics, containerId) { - const analyticsHtml = jobAnalyticsHtml(jobAnalytics, jobHelpers) - $(containerId).html(analyticsHtml); -} - function renderJobsEfficiency(context) { const newConext = _.merge(context, {unit: "jobs", unit_title: "Jobs"}); const templateSource = $('#job-efficiency-template').html(); @@ -254,7 +241,7 @@ function createJobsWidget() { console.error(error); renderJobs({error: error}); - reportError('xdmod_jobs_widget_error', error); + reportErrorForAnalytics('xdmod_jobs_widget_error', error); }); } @@ -268,7 +255,7 @@ function addAnalyticsToJob(jobId) { console.error(error); renderJobAnalytics({error: error}, analyticsContainer); - reportError('xdmod_jobs_analytics_widget_error', error); + reportErrorForAnalytics('xdmod_jobs_analytics_widget_error', error); }); } @@ -313,26 +300,14 @@ function createEfficiencyWidgets() { renderJobsEfficiency({error: error}); renderCoreHoursEfficiency({error: error}); - reportError('xdmod_jobs_widget_error', error); + reportErrorForAnalytics('xdmod_jobs_widget_error', error); }); } -function reportError(path, error) { - // error - report back for analytics purposes - const analyticsUrl = new URL(analyticsPath(path), document.location); - analyticsUrl.searchParams.append('error', error); - fetch(analyticsUrl); -} - jQuery(() => { createJobsWidget(); createEfficiencyWidgets(); // initialize the panels renderJobs({ loading: true }); - - $(`#${jobPanelId}`).on('click', 'tr[data-xdmod-jobid][aria-expanded="true"]', function(event) { - const jobId = event.currentTarget.getAttribute("data-xdmod-jobid"); - addAnalyticsToJob(jobId) - }); }); \ No newline at end of file diff --git a/apps/dashboard/app/javascript/xdmod/jobs.js b/apps/dashboard/app/javascript/xdmod/jobs.js index 6c3cc1cb7..54401f59a 100644 --- a/apps/dashboard/app/javascript/xdmod/jobs.js +++ b/apps/dashboard/app/javascript/xdmod/jobs.js @@ -1,5 +1,7 @@ 'use strict'; +import {reportErrorForAnalytics} from '../utils'; + export function jobsPanel(context, helpers){ const div = document.createElement('div'); div.classList.add('xdmod'); @@ -9,26 +11,6 @@ export function jobsPanel(context, helpers){ return div; } -export function jobAnalyticsHtml(context, jobHelpers) { - if(context.error !== undefined) { - return errorBody(context.error, jobHelpers); - } - - const dataByKey = context.data.reduce((acc, obj) => { - acc[obj.key] = obj; - return acc; - }, {}); - const cpuEfficiency = jobHelpers.efficiency_label(dataByKey['CPU User']?.value, false) - const memEfficiency = jobHelpers.efficiency_label(dataByKey['Memory Headroom']?.value, true) - const walltimeEfficiency = jobHelpers.efficiency_label(dataByKey['Walltime Accuracy']?.value, false) - const analyticsContent = `
- CPU: ${cpuEfficiency} - Mem: ${memEfficiency} - Walltime: ${walltimeEfficiency} -
`; - return analyticsContent -} - function card(context, helpers) { const div = document.createElement('div'); div.classList.add('card', 'mt-3'); @@ -140,6 +122,11 @@ function tableRows(context, helpers) { tr.setAttribute('data-bs-target', `#details_${job.jobid}`); tr.setAttribute('aria-expanded', 'false'); + tr.addEventListener('click', function(event) { + const jobId = event.currentTarget.getAttribute("data-xdmod-jobid"); + getJobAnalytics(jobId, helpers) + }, { once: true }); + // Job analytics collapse icons const td0 = document.createElement('td'); td0.innerHTML = ` @@ -213,3 +200,48 @@ function noDataRow() { return tr; } + +function renderJobAnalytics(context, containerId, helpers) { + if(context.error !== undefined) { + const errorMessage = errorBody(context.error, helpers); + document.getElementById(containerId).replaceChildren(errorMessage); + return; + } + + const dataByKey = context.data.reduce((acc, obj) => { + acc[obj.key] = obj; + return acc; + }, {}); + const cpuEfficiency = helpers.efficiency_label(dataByKey['CPU User']?.value, false) + const memEfficiency = helpers.efficiency_label(dataByKey['Memory Headroom']?.value, true) + const walltimeEfficiency = helpers.efficiency_label(dataByKey['Walltime Accuracy']?.value, false) + const div = document.createElement('div'); + div.classList.add('job-analytics'); + div.innerHTML = `CPU: ${cpuEfficiency} + Mem: ${memEfficiency} + Walltime: ${walltimeEfficiency}`; + + document.getElementById(containerId).replaceChildren(div); +} + +function jobAnalyticsUrl(jobId, helpers){ + let url = new URL(`${helpers.xdmod_url()}/rest/v1.0/warehouse/search/jobs/analytics`); + url.searchParams.set('_dc', Date.now()); + url.searchParams.set('realm', helpers.realm); + url.searchParams.set('jobid', jobId); + return url; +} + +function getJobAnalytics(jobId, helpers) { + const analyticsContainer = `details_${jobId}`; + fetch(jobAnalyticsUrl(jobId, helpers), { credentials: 'include' }) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then((data) => renderJobAnalytics(data, analyticsContainer, helpers)) + .catch((error) => { + console.error(error); + renderJobAnalytics({error: error}, analyticsContainer, helpers); + + reportErrorForAnalytics('xdmod_jobs_analytics_widget_error', error); + }); +} From fdbdd41fc1de1ff109df16e09a420c6c9d214553 Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Tue, 24 Sep 2024 15:02:36 +0100 Subject: [PATCH 6/7] CSS improvement for job analytic metrics badges --- apps/dashboard/app/assets/stylesheets/xdmod.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dashboard/app/assets/stylesheets/xdmod.scss b/apps/dashboard/app/assets/stylesheets/xdmod.scss index d1b2249f5..396584997 100644 --- a/apps/dashboard/app/assets/stylesheets/xdmod.scss +++ b/apps/dashboard/app/assets/stylesheets/xdmod.scss @@ -26,5 +26,9 @@ strong { font-weight: 600; } + + .badge { + vertical-align: 1px; + } } } \ No newline at end of file From 5254d559af61a0d31b063f621dc80573cf41317d Mon Sep 17 00:00:00 2001 From: Aday Bujeda Date: Tue, 24 Sep 2024 15:29:48 +0100 Subject: [PATCH 7/7] Fixed flaky models/Project test --- apps/dashboard/test/models/projects_test.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/dashboard/test/models/projects_test.rb b/apps/dashboard/test/models/projects_test.rb index aaea1faea..0acb8c9f1 100644 --- a/apps/dashboard/test/models/projects_test.rb +++ b/apps/dashboard/test/models/projects_test.rb @@ -134,7 +134,7 @@ class ProjectsTest < ActiveSupport::TestCase test 'creates manifest.yml in .ondemand config directory' do Dir.mktmpdir do |tmp| projects_path = Pathname.new(tmp) - project = create_project(projects_path) + project = create_project(projects_path, id: "test-#{Project.next_id}") assert project.errors.inspect assert_equal "#{projects_path}/projects/#{project.id}", project.directory.to_s @@ -172,7 +172,8 @@ class ProjectsTest < ActiveSupport::TestCase test 'update project manifest.yml file' do Dir.mktmpdir do |tmp| projects_path = Pathname.new(tmp) - project = create_project(projects_path) + + project = create_project(projects_path, id: "test-#{Project.next_id}") name = 'test-project-2' description = 'my test project'