diff --git a/apps/dashboard/app/controllers/active_jobs_controller.rb b/apps/dashboard/app/controllers/active_jobs_controller.rb index 9672513198..6748ef35de 100644 --- a/apps/dashboard/app/controllers/active_jobs_controller.rb +++ b/apps/dashboard/app/controllers/active_jobs_controller.rb @@ -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}") 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 @@ -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 diff --git a/apps/dashboard/app/helpers/system_status_helper.rb b/apps/dashboard/app/helpers/system_status_helper.rb index 29f2f34bb0..5d4dfe3caf 100644 --- a/apps/dashboard/app/helpers/system_status_helper.rb +++ b/apps/dashboard/app/helpers/system_status_helper.rb @@ -3,12 +3,16 @@ # 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 @@ -16,7 +20,7 @@ def status_hash(name, active, total) 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 @@ -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 @@ -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) @@ -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) diff --git a/apps/dashboard/app/javascript/active_jobs.js b/apps/dashboard/app/javascript/active_jobs.js index a028e0c91a..5376234b37 100644 --- a/apps/dashboard/app/javascript/active_jobs.js +++ b/apps/dashboard/app/javascript/active_jobs.js @@ -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'); @@ -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); @@ -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); } @@ -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`; + let ariaLabel = activeJobsConfig.toggleVisibility.replace('%{jobname}', escapeHtml(jobname)).replace('%{cluster_title}', cluster_title); + return ``; }, }, { @@ -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 = ` @@ -288,17 +297,19 @@ function create_datatable(options){ // This will be empty when support ticket is disabled. return `
${support_ticket}
`; } else { + let confirmText = activeJobsConfig.deleteConfirm.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid); + let ariaDelete = activeJobsConfig.deleteAria.replace('%{jobname}', escapeHtml(jobname)).replace('%{pbsid}', pbsid); return `
@@ -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 diff --git a/apps/dashboard/app/javascript/application.js b/apps/dashboard/app/javascript/application.js index fc663208f8..b51f9220a1 100644 --- a/apps/dashboard/app/javascript/application.js +++ b/apps/dashboard/app/javascript/application.js @@ -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(){ diff --git a/apps/dashboard/app/javascript/config.js b/apps/dashboard/app/javascript/config.js index cb9395c2e8..885df9955c 100644 --- a/apps/dashboard/app/javascript/config.js +++ b/apps/dashboard/app/javascript/config.js @@ -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' diff --git a/apps/dashboard/app/javascript/files/clip_board.js b/apps/dashboard/app/javascript/files/clip_board.js index 40c2846c1d..f443bc0f83 100755 --- a/apps/dashboard/app/javascript/files/clip_board.js +++ b/apps/dashboard/app/javascript/files/clip_board.js @@ -1,11 +1,11 @@ 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', @@ -13,7 +13,12 @@ const EVENTNAME = { 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(); @@ -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(); }); @@ -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 { @@ -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 ${clipboard.from} to the current directory:`; + description.innerHTML = filesLabels.labelClipboardDescription.replace('__FROM__', clipboard.from); cardBody.appendChild(description); card.appendChild(cardBody); @@ -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'; @@ -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); diff --git a/apps/dashboard/app/javascript/files/data_table.js b/apps/dashboard/app/javascript/files/data_table.js index 6f167580fc..20c402cdf6 100644 --- a/apps/dashboard/app/javascript/files/data_table.js +++ b/apps/dashboard/app/javascript/files/data_table.js @@ -15,8 +15,12 @@ const CONTENTID = '#directory-contents'; const SPINNERID = '#tloading_spinner'; let table = null; +let filesLabels = null; jQuery(function () { + const configEl = document.getElementById('files_page_load_config'); + if (!configEl) return; + filesLabels = configEl.dataset; table = new DataTable(); /* END BUTTON ACTIONS */ @@ -189,7 +193,7 @@ class DataTable { }).DataTable({ autoWidth: false, language: { - search: 'Filter:', + search: filesLabels.labelFilter, infoFiltered: "" }, order: [[1, "asc"], [2, "asc"]], @@ -221,10 +225,10 @@ class DataTable { orderable: false, defaultContent: '', render: (data, type, row, meta) => { - return ``; + return ``; } }, - { data: 'type', render: (data, type, row, meta) => data == 'd' ? 'directory' : 'file' }, // type + { data: 'type', render: (data, type, row, meta) => data == 'd' ? `${filesLabels.labelDirectory}` : `${filesLabels.labelFile}` }, // type { name: 'name', data: 'name', className: 'text-break', render: (data, type, row, meta) => this.renderNameColumn(data, type, row, meta) }, // name { name: 'actions', orderable: false, searchable: false, data: null, render: (data, type, row, meta) => this.actionsBtnTemplate({ row_index: meta.row, file: row.type != 'd', data: row }) }, { @@ -239,7 +243,7 @@ class DataTable { let date = new Date(data * 1000) // Return formatted date "3/23/2021 10:52:28 AM" - return isNaN(data) ? 'Invalid Date' : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` + return isNaN(data) ? filesLabels.labelInvalidDate : `${date.toLocaleDateString()} ${date.toLocaleTimeString()}` } else { return data; @@ -262,8 +266,8 @@ class DataTable { ] }); - $('div.dt-search').prepend(``) - $('div.dt-search').prepend(``) + $('div.dt-search').prepend(``) + $('div.dt-search').prepend(``) this.updateGlobus(); } @@ -303,8 +307,8 @@ class DataTable { $('#select_all').trigger(); } - $(`${CONTENTID}_caption`).text(`Contents of directory ${data.path}`); - ariaNotify(`navigated to ${data.path}`); + $(`${CONTENTID}_caption`).text(filesLabels.labelContentsOfDirectory.replace('__PATH__', data.path)); + ariaNotify(filesLabels.labelNavigatedTo.replace('__PATH__', data.path)); let result = await Promise.resolve(data); $('td input[type=checkbox]').on('keypress', function(event) { @@ -384,7 +388,7 @@ class DataTable { if(disposition === null) { return response; } else { - throw new Error("Cannot navigate to a file."); + throw new Error(filesLabels.labelCannotNavigateToFile); } }) .then(response => response.json()) @@ -444,7 +448,7 @@ class DataTable { button.setAttribute('data-bs-toggle', 'dropdown'); button.setAttribute('aria-haspopup', 'true'); button.setAttribute('aria-expanded', 'false'); - button.setAttribute('title', 'Actions'); + button.setAttribute('title', filesLabels.labelActions); // Create the icon inside the button const icon = document.createElement('span'); @@ -466,7 +470,7 @@ class DataTable { viewLink.href = data.url; viewLink.classList.add('view-file', 'dropdown-item'); viewLink.setAttribute('data-row-index', rowIndex); - viewLink.innerHTML = ' View'; + viewLink.innerHTML = ` ${filesLabels.labelView}`; viewItem.appendChild(viewLink); dropdownMenu.appendChild(viewItem); } @@ -477,7 +481,7 @@ class DataTable { editLink.href = data.edit_url; editLink.classList.add('edit-file', 'dropdown-item'); editLink.setAttribute('data-row-index', rowIndex); - editLink.innerHTML = ' Edit'; + editLink.innerHTML = ` ${filesLabels.labelEdit}`; editItem.appendChild(editLink); dropdownMenu.appendChild(editItem); } @@ -489,7 +493,7 @@ class DataTable { renameLink.href = '#'; renameLink.classList.add('rename-file', 'dropdown-item'); renameLink.setAttribute('data-row-index', rowIndex); - renameLink.innerHTML = ' Rename'; + renameLink.innerHTML = ` ${filesLabels.labelRename}`; renameItem.appendChild(renameLink); dropdownMenu.appendChild(renameItem); @@ -500,7 +504,7 @@ class DataTable { downloadLink.href = data.download_url; downloadLink.classList.add('download-file', 'dropdown-item'); downloadLink.setAttribute('data-row-index', rowIndex); - downloadLink.innerHTML = ' Download'; + downloadLink.innerHTML = ` ${filesLabels.labelDownload}`; downloadItem.appendChild(downloadLink); dropdownMenu.appendChild(downloadItem); } @@ -516,7 +520,7 @@ class DataTable { deleteLink.href = '#'; deleteLink.classList.add('delete-file', 'dropdown-item', 'text-danger'); deleteLink.setAttribute('data-row-index', rowIndex); - deleteLink.innerHTML = ' Delete'; + deleteLink.innerHTML = ` ${filesLabels.labelDelete}`; deleteItem.appendChild(deleteLink); dropdownMenu.appendChild(deleteItem); @@ -532,9 +536,9 @@ class DataTable { let api = this._table; let rows = api.rows({ selected: true }).flatten().length, page_info = api.page.info(), - msg = page_info.recordsTotal == page_info.recordsDisplay ? `Showing ${page_info.recordsDisplay} rows` : `Showing ${page_info.recordsDisplay} of ${page_info.recordsTotal} rows`; + msg = page_info.recordsTotal == page_info.recordsDisplay ? filesLabels.labelShowingRows.replace('__SHOWN__', page_info.recordsDisplay) : filesLabels.labelShowingRowsFiltered.replace('__SHOWN__', page_info.recordsDisplay).replace('__TOTAL__', page_info.recordsTotal); - $('#directory-contents_info').html(`${msg} - ${rows} rows selected`); + $('#directory-contents_info').html(`${msg} - ${filesLabels.labelRowsSelected.replace('__COUNT__', rows)}`); } goto(url, pushState = true, show_processing_indicator = true) { diff --git a/apps/dashboard/app/javascript/files/file_ops.js b/apps/dashboard/app/javascript/files/file_ops.js index 451b200c17..2bdf4f62f2 100644 --- a/apps/dashboard/app/javascript/files/file_ops.js +++ b/apps/dashboard/app/javascript/files/file_ops.js @@ -1,11 +1,11 @@ -import {CONTENTID, EVENTNAME as DATATABLE_EVENTNAME} from './data_table.js'; -import {EVENTNAME as CLIPBOARD_EVENTNAME} from './clip_board.js'; -import {EVENTNAME as SWAL_EVENTNAME} from './sweet_alert.js'; +import { CONTENTID, EVENTNAME as DATATABLE_EVENTNAME } from './data_table.js'; +import { EVENTNAME as CLIPBOARD_EVENTNAME } from './clip_board.js'; +import { EVENTNAME as SWAL_EVENTNAME } from './sweet_alert.js'; import _ from 'lodash'; import { transfersPath, csrfToken } from '../config.js'; import { OODAlertError } from '../alert'; -export {EVENTNAME}; +export { EVENTNAME }; const EVENTNAME = { changeDirectory: 'changeDirectory', @@ -25,56 +25,59 @@ const EVENTNAME = { renameFilePrompt: 'renameFilePrompt', } - +let filesLabels = null; let fileOps = null; -export {fileOps}; +export { fileOps }; -jQuery(function() { - fileOps = new FileOps(); +jQuery(function () { + const configEl = document.getElementById('files_page_load_config'); + if (!configEl) return; + filesLabels = configEl.dataset; + fileOps = new FileOps(); - $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function(event){ - if(fileOps.clickEventIsSignificant(event)){ + $('#directory-contents tbody, #path-breadcrumbs, #favorites').on('click', 'a.d', function (event) { + if (fileOps.clickEventIsSignificant(event)) { event.preventDefault(); event.cancelBubble = true; - if(event.stopPropagation) event.stopPropagation(); + if (event.stopPropagation) event.stopPropagation(); const eventData = { 'path': this.getAttribute("href"), }; - + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); - + } }); $('#directory-contents tbody').on('click', 'tr td:first-child input[type=checkbox]', function (e) { if (this.dataset['dlUrl'] == 'undefined' && this.checked) { $("#download-btn").attr('disabled', true); - } else if ($("input[data-dl-url='undefined']:checked" ).length == 0) { + } else if ($("input[data-dl-url='undefined']:checked").length == 0) { $("#download-btn").attr('disabled', false); } }); - - $('#directory-contents tbody').on('dblclick', 'tr td:not(:first-child)', function(){ + + $('#directory-contents tbody').on('dblclick', 'tr td:not(:first-child)', function () { // handle double-click let a = this.parentElement.querySelector('a'); - if(a.classList.contains('d')) { + if (a.classList.contains('d')) { const eventData = { 'path': a.getAttribute("href"), }; - + $(CONTENTID).trigger(DATATABLE_EVENTNAME.goto, eventData); } }); - $('#directory-contents tbody').on('click', '.download-file', function(e){ + $('#directory-contents tbody').on('click', '.download-file', function (e) { e.preventDefault(); const table = $(CONTENTID).DataTable(); const row = e.currentTarget.dataset.rowIndex; const eventData = { - selection: table.rows(row).data() + selection: table.rows(row).data() }; $(CONTENTID).trigger(EVENTNAME.download, eventData); @@ -89,14 +92,14 @@ jQuery(function() { }); $("#new-dir-btn").on("click", function () { - $(CONTENTID).trigger(EVENTNAME.newDirectoryPrompt); + $(CONTENTID).trigger(EVENTNAME.newDirectoryPrompt); }); $("#download-btn").on("click", function () { let table = $(CONTENTID).DataTable(); let selection = table.rows({ selected: true }).data(); const eventData = { - selection: selection + selection: selection }; $(CONTENTID).trigger(EVENTNAME.download, eventData); @@ -108,7 +111,7 @@ jQuery(function() { let table = $(CONTENTID).DataTable(); let files = table.rows({ selected: true }).data().toArray().map((f) => f.name); const eventData = { - files: files + files: files }; $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); @@ -116,7 +119,7 @@ jQuery(function() { }); $(document).on("click", '#goto-btn', function () { - $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); + $(CONTENTID).trigger(EVENTNAME.changeDirectoryPrompt); }); $(document).on('click', '.rename-file', function (e) { @@ -127,25 +130,25 @@ jQuery(function() { let fileName = $($.parseHTML(row.name)).text(); const eventData = { - file: fileName, + file: fileName, }; - + $(CONTENTID).trigger(EVENTNAME.renameFilePrompt, eventData); }); $(document).on('click', '.delete-file', function (e) { - e.preventDefault(); - let table = $(CONTENTID).DataTable(); - let rowId = e.currentTarget.dataset.rowIndex; - let row = table.row(rowId).data(); - let fileName = $($.parseHTML(row.name)).text(); + e.preventDefault(); + let table = $(CONTENTID).DataTable(); + let rowId = e.currentTarget.dataset.rowIndex; + let row = table.row(rowId).data(); + let fileName = $($.parseHTML(row.name)).text(); - const eventData = { - files: [fileName] - }; + const eventData = { + files: [fileName] + }; - $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); + $(CONTENTID).trigger(EVENTNAME.deletePrompt, eventData); }); @@ -174,22 +177,22 @@ jQuery(function() { }); $(CONTENTID).on(EVENTNAME.download, function (e, options) { - if(options.selection.length == 0) { - OODAlertError('Select a file, files, or directory to download. You have selected none.'); + if (options.selection.length == 0) { + OODAlertError(filesLabels.labelDownloadErrorMsg); } else { fileOps.download(options.selection); } }); $(CONTENTID).on(EVENTNAME.deletePrompt, function (e, options) { - if(options.files.length == 0) { - OODAlertError('Select a file, files, or directory to delete. You have selected none.'); + if (options.files.length == 0) { + OODAlertError(filesLabels.labelDeleteErrorMsg); } else { fileOps.deletePrompt(options.files); } }); - $(CONTENTID).on(EVENTNAME.deleteFile, function (e, options) { + $(CONTENTID).on(EVENTNAME.deleteFile, function (e, options) { fileOps.delete(options.files, options.from_fs); }); @@ -223,7 +226,7 @@ class FileOps { clickEventIsSignificant(event) { return !( // (event.target && (event.target as any).isContentEditable) - event.defaultPrevented + event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey @@ -245,18 +248,18 @@ class FileOps { const eventData = { action: 'changeDirectory', 'inputOptions': { - title: 'Change Directory', + title: filesLabels.labelChangeDirectoryTitle, input: 'text', - inputLabel: 'Path', - inputValue: history.state.currentDirectory, + inputLabel: filesLabels.labelPath, + inputValue: history.state.currentDirectory, inputAttributes: { spellcheck: 'false', }, showCancelButton: true, inputValidator: (value) => { - if (! value || ! value.startsWith('/')) { + if (!value || !value.startsWith('/')) { // TODO: validate filenames against listing - return 'Provide an absolute pathname' + return filesLabels.labelProvideAbsolutePath } } } @@ -271,8 +274,8 @@ class FileOps { action: EVENTNAME.deleteFile, files: files, 'inputOptions': { - title: files.length == 1 ? `Delete ${files[0]}?` : `Delete ${files.length} selected files?`, - text: 'Are you sure you want to delete the files: ' + files.join(', '), + title: files.length == 1 ? filesLabels.labelDeleteSingle.replace('__FILE__', files[0]) : filesLabels.labelDeleteMultiple.replace('__COUNT__', files.length), + text: filesLabels.labelDeleteConfirm.replace('__FILES__', files.join(', ')), showCancelButton: true, } }; @@ -281,10 +284,10 @@ class FileOps { } } - + removeFiles(files) { this.transferFiles(files, "rm", "remove files", history.state.currentFilesystem) - } + } renameFile(fileName, newFileName) { let files = {}; @@ -297,20 +300,20 @@ class FileOps { action: EVENTNAME.renameFile, files: fileName, 'inputOptions': { - title: 'Rename', + title: filesLabels.labelRename, input: 'text', - inputLabel: 'Filename', + inputLabel: filesLabels.labelFilename, inputValue: fileName, inputAttributes: { spellcheck: 'false', }, showCancelButton: true, inputValidator: (value) => { - if (! value) { + if (!value) { // TODO: validate filenames against listing - return 'Provide a filename to rename this to'; + return filesLabels.labelProvideRenameFilename; } else if (value.includes('/') || value.includes('..')) { - return 'Filename cannot include / or ..'; + return filesLabels.labelFilenameIllegalChars; } } } @@ -327,18 +330,18 @@ class FileOps { const eventData = { action: EVENTNAME.createFile, 'inputOptions': { - title: 'New File', + title: filesLabels.labelNewFile, input: 'text', - inputLabel: 'Filename', + inputLabel: filesLabels.labelFilename, showCancelButton: true, inputValidator: (value) => { if (!value) { // TODO: validate filenames against listing - return 'Provide a non-empty filename.' + return filesLabels.labelProvideNonemptyFilename } else if (value.includes("/")) { // TODO: validate filenames against listing - return 'Illegal character (/) not allowed in filename.' + return filesLabels.labelFilenameIllegalSlash } } } @@ -356,7 +359,7 @@ class FileOps { myFileOp.reloadTable(); }) .catch(function (e) { - OODAlertError(`Error occurred when attempting to create new file: ${e.message}`); + OODAlertError(filesLabels.labelErrorCreateFile.replace('__ERROR__', e.message)); }); } @@ -365,14 +368,14 @@ class FileOps { const eventData = { action: EVENTNAME.createDirectory, 'inputOptions': { - title: 'New Directory', + title: filesLabels.labelNewDirectory, input: 'text', - inputLabel: 'Directory name', + inputLabel: filesLabels.labelDirectoryName, showCancelButton: true, inputValidator: (value) => { if (!value || value.includes("/")) { // TODO: validate filenames against listing - return 'Provide a directory name that does not have / in it' + return filesLabels.labelProvideDirectoryName } } } @@ -384,19 +387,19 @@ class FileOps { newDirectory(filename) { let myFileOp = new FileOps(); - fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, {method: 'put', headers: { 'X-CSRF-Token': csrfToken() }}) + fetch(`${history.state.currentDirectoryUrl}/${encodeURI(filename)}?dir=true`, { method: 'put', headers: { 'X-CSRF-Token': csrfToken() } }) .then(response => this.dataFromJsonResponse(response)) .then(function () { myFileOp.reloadTable(); }) .catch(function (e) { - OODAlertError(`Error occurred when attempting to create new directory: ${e.message}`); + OODAlertError(filesLabels.labelErrorCreateDirectory.replace('__ERROR__', e.message)); }); } download(selection) { - selection.toArray().forEach( (f) => { - if(f.type == 'd') { + selection.toArray().forEach((f) => { + if (f.type == 'd') { this.downloadDirectory(f); } else if (f.type == 'f') { this.downloadFile(f); @@ -406,17 +409,17 @@ class FileOps { downloadDirectory(file) { let filename = $($.parseHTML(file.name)).text(), - canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + canDownloadReq = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?can_download=${Date.now().toString()}` + + this.showSwalLoading(filesLabels.labelPreparingDownload.replace('__NAME__', file.name)); - this.showSwalLoading('preparing to download directory: ' + file.name); - fetch(canDownloadReq, { - method: 'GET', - headers: { - 'X-CSRF-Token': csrfToken(), - 'Accept': 'application/json' - } - }) + method: 'GET', + headers: { + 'X-CSRF-Token': csrfToken(), + 'Accept': 'application/json' + } + }) .then(response => this.dataFromJsonResponse(response)) .then(data => { if (data.can_download) { @@ -424,58 +427,58 @@ class FileOps { this.downloadFile(file) } else { this.doneLoading(); - OODAlertError(`Error while downloading: ${data.error_message}`); + OODAlertError(filesLabels.labelErrorDownloading.replace('__ERROR__', data.error_message)); } }) .catch(e => { this.doneLoading(); - OODAlertError(`Error while downloading: ${e.message}`); + OODAlertError(filesLabels.labelErrorDownloading.replace('__ERROR__', e.message)); }) } - - + + downloadFile(file) { // creating the temporary iframe is exactly what the CloudCmd does // so this just repeats the status quo - + let filename = $($.parseHTML(file.name)).text(), - downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, - iframe = document.createElement('iframe'), - TIME = 30 * 1000; - + downloadUrl = `${history.state.currentDirectoryUrl}/${encodeURI(filename)}?download=${Date.now().toString()}`, + iframe = document.createElement('iframe'), + TIME = 30 * 1000; + iframe.setAttribute('class', 'd-none'); iframe.setAttribute('src', downloadUrl); - + document.body.appendChild(iframe); - - setTimeout(function() { + + setTimeout(function () { document.body.removeChild(iframe); }, TIME); } - + dataFromJsonResponse(response) { return new Promise((resolve, reject) => { - Promise.resolve(response) - .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) - .then(response => response.json()) - .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) - .catch((e) => reject(e)) + Promise.resolve(response) + .then(response => response.ok ? Promise.resolve(response) : Promise.reject(new Error(response.statusText))) + .then(response => response.json()) + .then(data => data.error_message ? Promise.reject(new Error(data.error_message)) : resolve(data)) + .catch((e) => reject(e)) }); } - - + + delete(files) { - this.showSwalLoading('Deleting files...: '); + this.showSwalLoading(filesLabels.labelDeletingFiles); - this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrfToken() ); + this.removeFiles(files.map(f => [history.state.currentDirectory, f].join('/')), csrfToken()); } - transferFiles(files, action, summary, from_fs, to_fs){ + transferFiles(files, action, summary, from_fs, to_fs) { this._failures = 0; this.showSwalLoading(_.startCase(summary)); - + return fetch(transfersPath(), { method: 'post', body: JSON.stringify({ @@ -486,61 +489,61 @@ class FileOps { }), headers: { 'X-CSRF-Token': csrfToken() } }) - .then(response => this.dataFromJsonResponse(response)) - .then((data) => { - - if(! data.completed){ - // was async, gotta report on progress and start polling - this.reportTransfer(data); - this.findAndUpdateTransferStatus(data); - } else { - // if(data.target_dir == history.state.currentDirectory){ - // } - // this.findAndUpdateTransferStatus(data); - } - - if(action == 'mv' || action == 'cp') { - this.reloadTable(); - this.clearClipboard(); - this.updateClipboard(); - } + .then(response => this.dataFromJsonResponse(response)) + .then((data) => { - this.fadeOutTransferStatus(data); - this.doneLoading(); - this.reloadTable(); + if (!data.completed) { + // was async, gotta report on progress and start polling + this.reportTransfer(data); + this.findAndUpdateTransferStatus(data); + } else { + // if(data.target_dir == history.state.currentDirectory){ + // } + // this.findAndUpdateTransferStatus(data); + } - }) - .then(() => this.doneLoading()) - .catch(e => { - this.doneLoading(); - OODAlertError(`Error occurred when attempting to ${summary}: ${e.message}`); - }) + if (action == 'mv' || action == 'cp') { + this.reloadTable(); + this.clearClipboard(); + this.updateClipboard(); + } + + this.fadeOutTransferStatus(data); + this.doneLoading(); + this.reloadTable(); + + }) + .then(() => this.doneLoading()) + .catch(e => { + this.doneLoading(); + OODAlertError(filesLabels.labelErrorTransfer.replace('__SUMMARY__', summary).replace('__ERROR__', e.message)); + }) } - + findAndUpdateTransferStatus(data) { let id = `#${data.id}`; - - if($(id).length){ + + if ($(id).length) { $(id).replaceWith(this.reportTransferTemplate(data)); - } else{ + } else { $('.transfers-status').append(this.reportTransferTemplate(data)); } } - - fadeOutTransferStatus(data){ + + fadeOutTransferStatus(data) { let id = `#${data.id}`; $(id).fadeOut(4000); } reportTransferTemplate(data) { let html = ''; - + if (data.completed) { if (data.error_summary) { html += ` @@ -42,20 +42,20 @@
-

Active Jobs

+

<%= t('dashboard.active_jobs.title', default: 'Active Jobs') %>

- - - - - - - - - - + + + + + + + + + +
DetailsIDNameUserAccountTime UsedQueueStatusClusterActions<%= t('dashboard.active_jobs.header_details', default: 'Details') %><%= t('dashboard.active_jobs.header_id', default: 'ID') %><%= t('dashboard.active_jobs.header_name', default: 'Name') %><%= t('dashboard.active_jobs.header_user', default: 'User') %><%= t('dashboard.active_jobs.header_account', default: 'Account') %><%= t('dashboard.active_jobs.header_time_used', default: 'Time Used') %><%= t('dashboard.active_jobs.header_queue', default: 'Queue') %><%= t('dashboard.active_jobs.header_status', default: 'Status') %><%= t('dashboard.active_jobs.header_cluster', default: 'Cluster') %><%= t('dashboard.active_jobs.header_actions', default: 'Actions') %>
@@ -70,6 +70,24 @@ data-console-log-performance-report='<%= Configuration.console_log_performance_report? %>' data-base-uri='<%= controller.relative_url_root %>' data-ood-version='<%= Configuration.ood_version %>' + data-label-hold='<%= t("dashboard.active_jobs.label_hold", default: "Hold") %>' + data-label-undetermined='<%= t("dashboard.active_jobs.label_undetermined", default: "Undetermined") %>' + data-error-status-code='<%= t("dashboard.active_jobs.error_status_code", default: "Request for jobs failed with status code: ") %>' + data-error-parsing='<%= t("dashboard.active_jobs.error_parsing", default: "Request for jobs failed due to body parsing error.") %>' + data-error-remote='<%= t("dashboard.active_jobs.error_remote", default: "There was an error getting data from the remote server.") %>' + data-search-filter='<%= t("dashboard.active_jobs.search_filter", default: "Filter: ") %>' + data-empty-table='<%= t("dashboard.active_jobs.empty_table", default: "No data available in table") %>' + data-info='<%= t("dashboard.active_jobs.info", default: "Showing _START_ to _END_ of _TOTAL_ entries") %>' + data-info-empty='<%= t("dashboard.active_jobs.info_empty", default: "Showing 0 to 0 of 0 entries") %>' + data-info-filtered='<%= t("dashboard.active_jobs.info_filtered", default: "(filtered from _MAX_ total entries)") %>' + data-length-menu='<%= t("dashboard.active_jobs.length_menu", default: "_MENU_ entries per page") %>' + data-zero-records='<%= t("dashboard.active_jobs.zero_records", default: "No matching records found") %>' + data-toggle-visibility='<%= t("dashboard.active_jobs.toggle_visibility", default: "Toggle visibility of job %{jobname} on %{cluster_title}") %>' + data-submit-ticket-title='<%= t("dashboard.active_jobs.submit_ticket_title", default: "Submit Support Ticket") %>' + data-submit-ticket-aria='<%= t("dashboard.active_jobs.submit_ticket_aria", default: "Submit support ticket for job with ID %{pbsid}") %>' + data-delete-confirm='<%= t("dashboard.active_jobs.delete_confirm", default: "Are you sure you want to delete %{jobname} - %{pbsid}") %>' + data-delete-aria='<%= t("dashboard.active_jobs.delete_aria", default: "Delete job %{jobname} with ID %{pbsid}") %>' + data-delete-title='<%= t("dashboard.active_jobs.delete_title", default: "Delete Job") %>' >
diff --git a/apps/dashboard/app/views/batch_connect/session_contexts/_form.html.erb b/apps/dashboard/app/views/batch_connect/session_contexts/_form.html.erb index 94a0c47853..c0106d1e99 100644 --- a/apps/dashboard/app/views/batch_connect/session_contexts/_form.html.erb +++ b/apps/dashboard/app/views/batch_connect/session_contexts/_form.html.erb @@ -4,7 +4,7 @@ <%= render "prefill_templates" if Configuration.bc_saved_settings? %> <% if Configuration.bc_dynamic_js? %> - The following form may change dynamically as options are selected. All changes will be announced and are reversible + <%= t('dashboard.batch_connect_form_dynamic_warning') %> <% end %> <%= bootstrap_form_for(@session_context, html: { autocomplete: "off", }) do |f| %> <% f.object.each do |attrib| %> diff --git a/apps/dashboard/app/views/batch_connect/session_contexts/_prefill_templates.html.erb b/apps/dashboard/app/views/batch_connect/session_contexts/_prefill_templates.html.erb index 18c6765deb..c556079d1e 100644 --- a/apps/dashboard/app/views/batch_connect/session_contexts/_prefill_templates.html.erb +++ b/apps/dashboard/app/views/batch_connect/session_contexts/_prefill_templates.html.erb @@ -51,7 +51,7 @@ diff --git a/apps/dashboard/app/views/batch_connect/session_contexts/new.html.erb b/apps/dashboard/app/views/batch_connect/session_contexts/new.html.erb index 6e1ebbc168..9789325a14 100644 --- a/apps/dashboard/app/views/batch_connect/session_contexts/new.html.erb +++ b/apps/dashboard/app/views/batch_connect/session_contexts/new.html.erb @@ -74,9 +74,7 @@ locals: {

<%= @app.title %> - <% unless @app.version.nil? %> - version: <%= @app.version %> - <% end %> + <%= t('dashboard.version', version: @app.version) %>

<%= OodAppkit.markdown.render(@app.description).html_safe %> diff --git a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_linux.html.erb b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_linux.html.erb index 06e5f82889..e938f9703d 100644 --- a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_linux.html.erb +++ b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_linux.html.erb @@ -1,33 +1,31 @@
    -
  1. Open a terminal window
  2. +
  3. <%= t('dashboard.batch_connect_vnc_open_terminal') %>
  4. <% if Configuration.native_vnc_login_host %>

    - Copy/paste in your terminal to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel') %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@<%= Configuration.native_vnc_login_host %>
    <% else %>

    - Copy/paste in your terminal and replace SSH_HOST with a - valid HPC login server to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel_replace_host_html').html_safe %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@SSH_HOST
    <% end %>
  5. <% if browser.platform == :mac %>
  6. -

    Use the code below to establish the VNC connection:

    +

    <%= t('dashboard.batch_connect_vnc_mac_connection_html').html_safe %>

    open vnc://:<%= connect.password %>@localhost:<%= localport %>
  7. <% else %>
  8. - Open a VNC client and connect to - localhost:<%= localport %> within the client + <%= t('dashboard.batch_connect_vnc_client_connect_html', port: localport).html_safe %>

  9. -

    Use the VNC password: <%= connect.password %>

    +

    <%= t('dashboard.batch_connect_vnc_password_html', password: connect.password).html_safe %>

  10. <% end %>
diff --git a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_mac.html.erb b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_mac.html.erb index 06e5f82889..e938f9703d 100644 --- a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_mac.html.erb +++ b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_mac.html.erb @@ -1,33 +1,31 @@
    -
  1. Open a terminal window
  2. +
  3. <%= t('dashboard.batch_connect_vnc_open_terminal') %>
  4. <% if Configuration.native_vnc_login_host %>

    - Copy/paste in your terminal to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel') %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@<%= Configuration.native_vnc_login_host %>
    <% else %>

    - Copy/paste in your terminal and replace SSH_HOST with a - valid HPC login server to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel_replace_host_html').html_safe %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@SSH_HOST
    <% end %>
  5. <% if browser.platform == :mac %>
  6. -

    Use the code below to establish the VNC connection:

    +

    <%= t('dashboard.batch_connect_vnc_mac_connection_html').html_safe %>

    open vnc://:<%= connect.password %>@localhost:<%= localport %>
  7. <% else %>
  8. - Open a VNC client and connect to - localhost:<%= localport %> within the client + <%= t('dashboard.batch_connect_vnc_client_connect_html', port: localport).html_safe %>

  9. -

    Use the VNC password: <%= connect.password %>

    +

    <%= t('dashboard.batch_connect_vnc_password_html', password: connect.password).html_safe %>

  10. <% end %>
diff --git a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_windows.html.erb b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_windows.html.erb index 04eedfa277..41beca29a7 100644 --- a/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_windows.html.erb +++ b/apps/dashboard/app/views/batch_connect/sessions/connections/_native_vnc_windows.html.erb @@ -1,29 +1,25 @@
  1. - Download any VNC viewer, - RealVNC - is a good option. + <%= t('dashboard.batch_connect_vnc_download_html', realvnc_link: 'RealVNC').html_safe %>
  2. <% if Configuration.native_vnc_login_host %>

    - Copy/paste in your terminal to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel') %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@<%= Configuration.native_vnc_login_host %>
    <% else %>

    - Copy/paste in your terminal and replace SSH_HOST with a - valid HPC login server to establish the SSH tunnel: + <%= t('dashboard.batch_connect_vnc_ssh_tunnel_replace_host_html').html_safe %>

    ssh -f -N -L <%= localport %>:<%= connect.host %>:<%= connect.port %> <%= ENV["USER"] %>@SSH_HOST
    <% end %> -

    For terminals in Windows you can use: Powershell, PuTTy and Windows Subsystem Linux distributions

    +

    <%= t('dashboard.batch_connect_vnc_windows_terminals_html', pwsh: 'Powershell', putty: 'PuTTy', wsl: 'Windows Subsystem Linux').html_safe %>

  3. - Open a VNC client and connect to - localhost:<%= localport %> within the client + <%= t('dashboard.batch_connect_vnc_client_connect_html', port: localport).html_safe %>
  4. -

    Use the VNC password: <%= connect.password %>

    +

    <%= t('dashboard.batch_connect_vnc_password_html', password: connect.password).html_safe %>

diff --git a/apps/dashboard/app/views/batch_connect/sessions/index.turbo_stream.erb b/apps/dashboard/app/views/batch_connect/sessions/index.turbo_stream.erb index 72e387157b..fdfadb1f65 100644 --- a/apps/dashboard/app/views/batch_connect/sessions/index.turbo_stream.erb +++ b/apps/dashboard/app/views/batch_connect/sessions/index.turbo_stream.erb @@ -17,7 +17,7 @@
- Loading... + <%= t('dashboard.loading') %>
diff --git a/apps/dashboard/app/views/batch_connect/shared/_app_list.html.erb b/apps/dashboard/app/views/batch_connect/shared/_app_list.html.erb index fb3c5273ac..e13c452926 100644 --- a/apps/dashboard/app/views/batch_connect/shared/_app_list.html.erb +++ b/apps/dashboard/app/views/batch_connect/shared/_app_list.html.erb @@ -1,5 +1,5 @@
"> -
<%= title %>
+
<%= t("dashboard.nav_group_#{title.downcase.gsub(' ','_')}", default: title) %>
<%- OodAppGroup.groups_for( @@ -11,7 +11,7 @@ <%= content_tag( "p", - app_group.title, + t("dashboard.nav_group_#{app_group.title.downcase.gsub(' ','_')}", default: app_group.title), class: "list-group-item mb-0 header border-0" ) unless app_group.title.blank? %> diff --git a/apps/dashboard/app/views/batch_connect/shared/_app_list_item.html.erb b/apps/dashboard/app/views/batch_connect/shared/_app_list_item.html.erb index 9e76e84aa6..c329936005 100644 --- a/apps/dashboard/app/views/batch_connect/shared/_app_list_item.html.erb +++ b/apps/dashboard/app/views/batch_connect/shared/_app_list_item.html.erb @@ -1,19 +1,25 @@ <% app.links.each do |link| %> + <% title_text = link.title.to_s %> + <% desc_text = link.description.to_s %> <%= link_to( link.url.to_s, class: "list-group-item list-group-item-action border-0 #{"active" if local_assigns[:current_url] == link.url}", - title: link.title, + title: title_text.presence, data: { 'bs-toggle': "popover", - 'bs-content': content_tag(:div, manifest_markdown(link.description), class: 'ood-appkit markdown'), + 'bs-content': content_tag( + :div, + manifest_markdown(desc_text), + class: 'ood-appkit markdown' + ), 'bs-html': true, 'bs-trigger': "hover focus", container: "body", }.merge(link.data) ) do %> - <%= icon_tag(link.icon_uri) %> <%= link.title %> + <%= icon_tag(link.icon_uri) %> <%= title_text %> <%- if show_owner -%> (<%= app.owner %>) <%- end -%> diff --git a/apps/dashboard/app/views/batch_connect/shared/_breadcrumb.html.erb b/apps/dashboard/app/views/batch_connect/shared/_breadcrumb.html.erb index 1756103a3a..d4c19ca6e4 100644 --- a/apps/dashboard/app/views/batch_connect/shared/_breadcrumb.html.erb +++ b/apps/dashboard/app/views/batch_connect/shared/_breadcrumb.html.erb @@ -7,17 +7,17 @@ <% end %> diff --git a/apps/dashboard/app/views/files/_breadcrumb.html.erb b/apps/dashboard/app/views/files/_breadcrumb.html.erb index 980ab92f98..9924249910 100644 --- a/apps/dashboard/app/views/files/_breadcrumb.html.erb +++ b/apps/dashboard/app/views/files/_breadcrumb.html.erb @@ -1,6 +1,6 @@ <% if file_counter == 0 %> <% end %> @@ -10,10 +10,10 @@ <%= path_segment_with_slash(@filesystem, file.basename.to_s, file_counter, file_count) %> <% else %>