Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added XDMoD analytics metrics to jobs widget #3789

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dashboard/app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
30 changes: 30 additions & 0 deletions apps/dashboard/app/assets/stylesheets/xdmod.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** 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;
}

.job-analytics {
display: flex;
justify-content: space-between;
padding: 0.50rem 0.5rem;

strong {
font-weight: 600;
}
}
}
81 changes: 58 additions & 23 deletions apps/dashboard/app/javascript/xdmod.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
import _ from 'lodash';
import {xdmodUrl, analyticsPath} from './config';
import {today, startOfYear, thirtyDaysAgo} from './utils';
import { jobsPanel } from './xdmod/jobs';
import { jobsPanel, jobAnalyticsHtml } from './xdmod/jobs';
johrstrom marked this conversation as resolved.
Show resolved Hide resolved
import Handlebars from 'handlebars';

const jobsPageLimit = 10;

const jobHelpers = {
realm: 'Jobs',
title: function(){
return "Recently Completed Jobs";
},
Expand Down Expand Up @@ -44,19 +45,19 @@ 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";
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";

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 = `<span class="badge bg-${severity}">${Handlebars.escapeExpression(value.toString().padStart(4,0))}</span>`;
Expand Down Expand Up @@ -84,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;
}
Expand All @@ -106,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){
Expand Down Expand Up @@ -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{
Expand All @@ -169,14 +171,22 @@ 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);
url.searchParams.set('params', JSON.stringify({person: user?.results?.person_id}));
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());
Expand Down Expand Up @@ -210,6 +220,11 @@ 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();
Expand Down Expand Up @@ -239,13 +254,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' })
johrstrom marked this conversation as resolved.
Show resolved Hide resolved
.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}`);
Expand All @@ -254,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())
Expand Down Expand Up @@ -287,17 +313,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 });

$(`#${jobPanelId}`).on('click', 'tr[data-xdmod-jobid][aria-expanded="true"]', function(event) {
const jobId = event.currentTarget.getAttribute("data-xdmod-jobid");
addAnalyticsToJob(jobId)
});
});
63 changes: 55 additions & 8 deletions apps/dashboard/app/javascript/xdmod/jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ 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 = `<div class="job-analytics">
<span><strong>CPU:</strong> ${cpuEfficiency}</span>
<span><strong>Mem:</strong> ${memEfficiency}</span>
<span><strong>Walltime:</strong> ${walltimeEfficiency}</span>
</div>`;
return analyticsContent
}

function card(context, helpers) {
const div = document.createElement('div');
div.classList.add('card', 'mt-3');
Expand Down Expand Up @@ -77,15 +97,16 @@ function table(context, helpers) {
// <table class="table table-sm table-striped table-condensed">
tableElement.classList.add('table', 'table-sm', 'table-striped', 'table-condensed');

thead = document.createElement('thead');
const thead = document.createElement('thead');
// Empty th to accommodate for the job analytics button
thead.innerHTML = '<tr> \
<th></th> \
<th>ID</th> \
<th>Name</th> \
<th>Date</th> \
<th>CPU</th> \
</tr>';

tbody = document.createElement('tbody');
const tbody = document.createElement('tbody');
tbody.append(...tableRows(context, helpers));

tableElement.append(thead);
Expand All @@ -97,12 +118,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 = [];

// <tr title="{{job_name}} - {{local_job_id}}">
// <td class="text-nowrap"><a target="_blank" href="{{job_url}}">{{local_job_id}}&nbsp;<span class="fa fa-external-link-square-alt"></span></a></td>
Expand All @@ -113,6 +134,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 = `
<button class="btn btn-default btn-xs">
<i class="fa fa-plus fa-fw app-icon closed" aria-hidden="true"></i>
<i class="fa fa-minus fa-fw app-icon open" aria-hidden="true"></i>
</button>`

// <td class="text-nowrap">
// <a target="_blank" href="{{job_url}}">{{local_job_id}}&nbsp;<span class="fa fa-external-link-square-alt"></span>
Expand All @@ -132,12 +166,25 @@ function tableRows(context, helpers) {
td3.innerText = helpers.date(job);

// <td>{{cpu_label cpu_user}}</td>
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');
analyticsRow.innerHTML = `
<td colspan="4" class="hiddenRow">
<div class="collapse" id="details_${job.jobid}">
<div class="job-analytics">
<span>LOADING...</span>
</div>
</div>
</td>`;
rows.push(analyticsRow);
});

return rows;
Expand Down
55 changes: 1 addition & 54 deletions apps/dashboard/app/views/widgets/_xdmod_widget_jobs.html.erb
Original file line number Diff line number Diff line change
@@ -1,59 +1,6 @@
<%= javascript_include_tag 'xdmod', nonce: true %>

<div class="xdmod">
<%# This XDMoD widget is rendered using Javascript %>
<div id="jobsPanelDiv"></div>

<script id="jobs-template" type="text/x-handlebars-template">
<div class="card mt-3">

<div class="card-header">
<a href="{{xdmod_url}}" class="float-end">Open XDMoD <span class="fa fa-external-link-square-alt"></span></a>
<h3>{{title}} - {{date_range}}</h3>
</div>

{{#if error}}
<div class="card-body">
<div class="alert alert-danger mb-0">{{error}} Please ensure you are <a href="{{xdmod_url}}">logged into Open XDMoD first</a>, and then try again.</div>
</div>
{{else}}
{{#if loading}}
<div class="card-body">
<p class="card-text">LOADING...</p>
</div>
{{else}}
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-striped table-condensed">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Date</th>
<th>CPU</th>
</tr>
</thead>
<tbody>
{{#each results}}
<tr title="{{job_name}} - {{local_job_id}}">
<td class="text-nowrap"><a target="_blank" href="{{job_url}}">{{local_job_id}}&nbsp;<span class="fa fa-external-link-square-alt"></span></a></td>
<td class="overflow-hidden d-inline-block text-truncate mw-150px">{{job_name}}</td>
<td>{{date}}</td>
<td>{{cpu_label cpu_user}}</td>
</tr>
{{else}}
<tr><td colspan="7">No data available.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>

<div class="card-footer">
Showing first {{page_limit}} of {{totalCount}} jobs. See <a href="{{xdmod_url}}">your Open XDMoD dashboard&nbsp;<span class="fa fa-external-link-square-alt"></span></a> for more information.
</div>
{{/if}}
{{/if}}
</div>
</script>

</div>
Loading