Skip to content
10 changes: 5 additions & 5 deletions apps/dashboard/app/controllers/active_jobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ def delete_job
# It takes a couple of seconds for the job to clear out
# Using the sleep to wait before reload
sleep(2.0)
redirect_to active_jobs_path, :notice => "Successfully deleted #{job_id}"
redirect_to active_jobs_path, :notice => t('dashboard.active_jobs.delete_success', job_id: job_id, default: "Successfully deleted %{job_id}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a configuration that will raise errors on missing internationalizations, so providing a default value is always going to be unnecessary

rescue StandardError
redirect_to active_jobs_path, :alert => "Failed to delete #{job_id}"
redirect_to active_jobs_path, :alert => t('dashboard.active_jobs.delete_failure_with_id', job_id: job_id, default: "Failed to delete %{job_id}")
end
else
redirect_to active_jobs_path, :alert => 'Failed to delete.'
redirect_to active_jobs_path, :alert => t('dashboard.active_jobs.delete_failure_generic', default: 'Failed to delete.')
end
end

Expand All @@ -82,12 +82,12 @@ def get_job(jobid, cluster)

ActiveJobs::Jobstatusdata.new(data, cluster, true)
rescue OodCore::JobAdapterError
OpenStruct.new(name: jobid, error: 'No job details because job has already left the queue.',
OpenStruct.new(name: jobid, error: t('dashboard.active_jobs.job_details_left_queue', default: 'No job details because job has already left the queue.'),
status: 'completed')
rescue StandardError => e
Rails.logger.info("#{e}:#{e.message}")
Rails.logger.info(e.backtrace.join("\n"))
OpenStruct.new(name: jobid, error: "No job details available.\n#{e.backtrace}", status: '')
OpenStruct.new(name: jobid, error: "#{t('dashboard.active_jobs.job_details_unavailable', default: 'No job details available.')}\n#{e.backtrace}", status: '')
end

# Returns the filter id from the parameter if it is valid
Expand Down
22 changes: 13 additions & 9 deletions apps/dashboard/app/helpers/system_status_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@
# Helpers for the system status page /dashboard/systemstatus
module SystemStatusHelper
def title(cluster)
"#{cluster.title} Cluster Status"
t('dashboard.system_status_cluster_title', cluster_title: cluster.title)
end

def status_hash(name, active, total)
def status_hash(component_key, active, total)
{
message: "#{name} Available: #{number_with_delimiter(total - active)}",
message: t(
'dashboard.system_status_component_available',
component: t("dashboard.system_status_components.#{component_key}"),
available: number_with_delimiter(total - active)
),
percent: percent(active, total)
}
end

def not_slurm_hash(job_adapter)
scheduler = job_adapter.class.name.demodulize
{
message: "Cluster information is not available with #{scheduler}. Currently, only Slurm clusters are supported.",
message: t('dashboard.system_status_not_supported', scheduler: scheduler),
percent: -1
}
end
Expand All @@ -29,9 +33,9 @@ def components_status(job_adapter)
end

[
status_hash('Nodes', cluster_info.active_nodes, cluster_info.total_nodes),
status_hash('Processors', cluster_info.active_processors, cluster_info.total_processors),
status_hash('GPUs', cluster_info.active_gpus, cluster_info.total_gpus)
status_hash(:nodes, cluster_info.active_nodes, cluster_info.total_nodes),
status_hash(:processors, cluster_info.active_processors, cluster_info.total_processors),
status_hash(:gpus, cluster_info.active_gpus, cluster_info.total_gpus)
]
end

Expand All @@ -51,7 +55,7 @@ def active_jobs(job_adapter)

def active_job_status(job_adapter)
num_jobs = number_with_delimiter(active_jobs(job_adapter))
"#{num_jobs} Jobs Running"
t('dashboard.system_status_jobs_running_with_count', count: num_jobs)
end

def queued_jobs(job_adapter)
Expand All @@ -62,7 +66,7 @@ def queued_jobs(job_adapter)

def queued_job_status(job_adapter)
num_jobs = number_with_delimiter(queued_jobs(job_adapter))
"#{num_jobs} Jobs Queued"
t('dashboard.system_status_jobs_queued_with_count', count: num_jobs)
end

def active_job_pct(job_adapter)
Expand Down
47 changes: 29 additions & 18 deletions apps/dashboard/app/javascript/active_jobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function fetch_job_data(tr, row, options) {
fetch(jobDataUrl, { headers: {
'Accpet': 'application/json',
}})
.then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Login failed: IDP redirect failed')))
.then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error('Request failed')))
.then(response => response.json())
.then(response => {
const ele = document.getElementById('job_details');
Expand Down Expand Up @@ -122,13 +122,13 @@ function fetch_table_data(table, options){
}
}).fail(function(errorReport){
if(errorReport.statusCode != null){
show_errors(["Request for jobs failed with status code: " + errorReport.statusCode]);
show_errors([activeJobsConfig.errorStatusCode + errorReport.statusCode]);
}
else{
//FIXME: this error appears even when the above 404 occurs, for example
// that is because a 404 response for json request returns a plain text response
// and parsing that as json fails
show_errors(["Request for jobs failed due to body parsing error."])
show_errors([activeJobsConfig.errorParsing]);
}

table.processing(false);
Expand All @@ -138,11 +138,11 @@ function fetch_table_data(table, options){

function status_label(status){
const labelClass = cssBadgeForState(status);
var label = "Undetermined"
var label = activeJobsConfig.labelUndetermined;

if(status === "queued_held") {
label = "Hold";
} else {
label = activeJobsConfig.labelHold;
} else if(status && status !== "undetermined") {
label = capitalizeFirstLetter(status);
}

Expand All @@ -158,12 +158,19 @@ function create_datatable(options){
$("#" + filter_id).addClass("active");
var table = $('#job_status_table').DataTable({
autoWidth: true, // Automatically calculate column width
"lengthMenu": [ [10, 25, 50, -1], [10, 25, 50, "All"] ], // Manually set size of particular columns
// Values only. DataTables 2 uses language.lengthLabels for the -1 label.
"lengthMenu": [10, 25, 50, -1],
"bStateSave": true, // Save user selected table state
"aaSorting": [], // Turn off auto sort.
"pageLength": 50, // Set the number of rows
"oLanguage": {
"sSearch": "Filter: "
"language": {
"search": activeJobsConfig.searchFilter,
"emptyTable": activeJobsConfig.emptyTable,
"info": activeJobsConfig.info,
"infoEmpty": activeJobsConfig.infoEmpty,
"infoFiltered": activeJobsConfig.infoFiltered,
"lengthMenu": activeJobsConfig.lengthMenu,
"zeroRecords": activeJobsConfig.zeroRecords
},
"fnInitComplete": function( oSettings ) {
for ( var i=0, iLen=oSettings.aoData.length ; i<iLen ; i++ )
Expand All @@ -189,7 +196,8 @@ function create_datatable(options){
"searchable": false,
render: function (data, type, row, meta) {
let { cluster_title, jobname, } = row
return `<button class="details-control fa fa-plus btn btn-default" aria-label="Toggle visibility of job ${escapeHtml(jobname)} on ${cluster_title}"></button>`;
let ariaLabel = activeJobsConfig.toggleVisibility.replace('%{jobname}', escapeHtml(jobname)).replace('%{cluster_title}', cluster_title);
return `<button class="details-control fa fa-plus btn btn-default" aria-label="${ariaLabel}"></button>`;
},
},
{
Expand Down Expand Up @@ -269,14 +277,15 @@ function create_datatable(options){
const support_url = new URL(support_path, document.location);
support_url.searchParams.set("job_id", pbsid);
support_url.searchParams.set("cluster", cluster);
let ariaSupport = activeJobsConfig.submitTicketAria.replace('%{pbsid}', pbsid);
support_ticket = `
<a
class="btn btn-primary btn-xs"
href="${escapeHtml(support_url.toString())}"
aria-labeled-by"title"
aria-label="Submit support ticket for job with ID ${pbsid}"
aria-labeled-by="title"
aria-label="${ariaSupport}"
data-toggle="tooltip"
title="Submit Support Ticket"
title="${activeJobsConfig.submitTicketTitle}"
>
<i class='fas fa-medkit fa-fw' aria-hidden='true'></i>
</a>
Expand All @@ -288,17 +297,19 @@ function create_datatable(options){
// This will be empty when support ticket is disabled.
return `<div>${support_ticket}</div>`;
} else {
let confirmText = activeJobsConfig.deleteConfirm.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid);
let ariaDelete = activeJobsConfig.deleteAria.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid);
return `
<div>
<a
class="btn btn-danger btn-xs"
data-method="delete"
data-confirm="Are you sure you want to delete ${escapeHtml(jobname)} - ${pbsid}"
data-confirm="${confirmText}"
href="${escapeHtml(delete_path)}"
aria-labeled-by"title"
aria-label="Delete job ${escapeHtml(jobname)} with ID ${pbsid}"
aria-labeled-by="title"
aria-label="${ariaDelete}"
data-toggle="tooltip"
title="Delete Job"
title="${activeJobsConfig.deleteTitle}"
>
<i class='fas fa-trash-alt fa-fw' aria-hidden='true'></i>
</a>
Expand All @@ -311,7 +322,7 @@ function create_datatable(options){
]
}).on( 'error.dt', function ( e, settings, techNote, message ) {
// Event is fired when there's an error loading or parsing the datatable.
show_errors(['There was an error getting data from the remote server.']);
show_errors([activeJobsConfig.errorRemote]);
} );

// Override the Datatables default error message functionality
Expand Down
22 changes: 22 additions & 0 deletions apps/dashboard/app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ import initPopovers from './popovers'
window.jQuery = jQuery;
window.$ = jQuery;

function setGlobalDatatablesLanguageDefaults() {
const configEl = document.getElementById('ood_config');
if (!configEl) return;

const raw = configEl.dataset.datatablesLanguage;
if (!raw) return;

let language;
try {
language = JSON.parse(raw);
} catch (_e) {
// If malformed, avoid breaking the whole app JS.
return;
}

if (!jQuery?.fn?.dataTable?.defaults) return;

jQuery.extend(true, jQuery.fn.dataTable.defaults, { language });
}

setGlobalDatatablesLanguageDefaults();

Rails.start();

jQuery(function(){
Expand Down
6 changes: 6 additions & 0 deletions apps/dashboard/app/javascript/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export function uppyLocale() {
return JSON.parse(cfgData['uppyLocale']);
}

export function xdmodI18n() {
const cfgData = configData();
const raw = cfgData['xdmodI18n'];
return raw ? JSON.parse(raw) : {};
}

export function isBCDynamicJSEnabled() {
const cfgData = configData();
return cfgData['bcDynamicJs'] == 'true'
Expand Down
29 changes: 17 additions & 12 deletions apps/dashboard/app/javascript/files/clip_board.js
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not modify javascript files for localization - we haven't built any foundational stuff for that yet.

Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import ClipboardJS from 'clipboard';
import {CONTENTID} from './data_table.js';
import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js';
import {EVENTNAME as FILEOPS_EVENTNAME} from './file_ops.js';
import { CONTENTID } from './data_table.js';
import { EVENTNAME as SWAL_EVENTNAME } from './sweet_alert.js';
import { EVENTNAME as FILEOPS_EVENTNAME } from './file_ops.js';
import { csrfToken } from '../config.js';
import { OODAlertError } from '../alert.js';

export {EVENTNAME};
export { EVENTNAME };

const EVENTNAME = {
clearClipboard: 'clearClipboard',
updateClipboard: 'updateClipboard',
updateClipboardView: 'updateClipboardView',
}

let filesLabels = null;

jQuery(function () {
const configEl = document.getElementById('files_page_load_config');
if (!configEl) return;
filesLabels = configEl.dataset;

var clipBoard = new ClipBoard();

Expand All @@ -30,8 +35,8 @@ jQuery(function () {
});


$(CONTENTID).on('success', function (e) {
$(e.trigger).tooltip({ title: 'Copied path to clipboard!', trigger: 'manual', placement: 'bottom' }).tooltip('show');
clipBoard.getClipBoard().on('success', function (e) {
$(e.trigger).tooltip({ title: filesLabels.labelCopySuccessful, trigger: 'manual', placement: 'bottom' }).tooltip('show');
setTimeout(() => $(e.trigger).tooltip('hide'), 2000);
e.clearSelection();
});
Expand All @@ -47,7 +52,7 @@ jQuery(function () {

$(CONTENTID).on(EVENTNAME.updateClipboard, function (e, options) {
if (options.selection.length == 0) {
OODAlertError('Select a file, files, or directory to copy or move. You have selected none.');
OODAlertError(filesLabels.labelCopyMoveError);
$(CONTENTID).trigger(EVENTNAME.clearClipboard);

} else {
Expand Down Expand Up @@ -114,12 +119,12 @@ class ClipBoard {
closeButton.type = 'button';
closeButton.className = 'btn-close';
closeButton.setAttribute('data-bs-dismiss', 'alert');
closeButton.setAttribute('aria-label', 'Close');
closeButton.setAttribute('aria-label', filesLabels.labelClipboardClose);
cardBody.appendChild(closeButton);

const description = document.createElement('p');
description.className = 'mt-4';
description.innerHTML = `Copy or move the files below from <code>${clipboard.from}</code> to the current directory:`;
description.innerHTML = filesLabels.labelClipboardDescription.replace('__FROM__', clipboard.from);
cardBody.appendChild(description);

card.appendChild(cardBody);
Expand All @@ -133,7 +138,7 @@ class ClipBoard {
listItem.className = 'list-group-item';

const icon = document.createElement('span');
icon.title = file.directory ? 'directory' : 'file';
icon.title = file.directory ? filesLabels.labelDirectory : filesLabels.labelFile;
icon.className = file.directory
? 'fa fa-folder color-gold'
: 'fa fa-file color-lightgrey';
Expand All @@ -154,13 +159,13 @@ class ClipBoard {
const copyButton = document.createElement('button');
copyButton.id = 'clipboard-copy-to-dir';
copyButton.className = 'btn btn-primary';
copyButton.textContent = 'Copy';
copyButton.textContent = filesLabels.labelClipboardCopy;
actionsBody.appendChild(copyButton);

const moveButton = document.createElement('button');
moveButton.id = 'clipboard-move-to-dir';
moveButton.className = 'btn btn-danger float-end';
moveButton.textContent = 'Move';
moveButton.textContent = filesLabels.labelClipboardMove;
actionsBody.appendChild(moveButton);

card.appendChild(actionsBody);
Expand Down
Loading
Loading