diff --git a/toolkit/crashreporter/content/crashes.css b/toolkit/crashreporter/content/crashes.css index 7ddcb5753354..1a958e296e08 100644 --- a/toolkit/crashreporter/content/crashes.css +++ b/toolkit/crashreporter/content/crashes.css @@ -8,59 +8,68 @@ min-width: 30em; max-width: 60em; } + table { - clear: both; - width: 90%; - margin: 0 auto; + width: 100%; padding-bottom: 2em; } -th { - font-size: 130%; - text-align: left; - white-space: nowrap; -} -th:-moz-locale-dir(rtl) { - text-align: right; -} -/* name */ -th:first-child { - padding-inline-end: 2em; -} -/* submitted */ -th:last-child { - text-align: center; -} -:link, :visited { - display: block; - min-height: 17px; -} -/* date */ -td:first-child + td { - width: 0; - padding-inline-start: 1em; - padding-inline-end: .5em; - white-space: nowrap; -} -/* time */ -td:last-child { - width: 0; - padding-inline-start: .5em; - white-space: nowrap; -} -#clear-reports { +.float-right { float: right; } -#clear-reports:-moz-locale-dir(rtl) { - float: left; +.hidden { + display: none; +} +.table-title-container { + align-items: center; + display: flex; + justify-content: space-between; +} +.wide-button { + display: block; + min-height: 32px; + padding-left: 30px; + padding-right: 30px; } - .submitting { background-image: url(chrome://global/skin/icons/loading.png); + background-position: center; background-repeat: no-repeat; - background-position: right; background-size: 16px; } +.submitting .submit-crash-button-label { + display: none; +} +.failed-to-submit { + color: #ca8695; +} + +a.button-as-link { + -moz-appearance: none; + min-height: 30px; + color: var(--in-content-text-color) !important; + border: 1px solid var(--in-content-box-border-color) !important; + border-radius: 2px; + background-color: var(--in-content-page-background); + line-height: 30px; + margin: 4px 8px; + /* Ensure font-size isn't overridden by widget styling (e.g. in forms.css) */ + font-size: 1em; +} +a.button-as-link:hover { + background-color: var(--in-content-box-background-hover) !important; + text-decoration: none; +} +h2.lighter-font-weight { + font-weight: lighter; +} + +html[dir=ltr] th { + text-align: left; +} +html[dir=rtl] th { + text-align: right; +} @media (min-resolution: 1.1dppx) { .submitting { diff --git a/toolkit/crashreporter/content/crashes.js b/toolkit/crashreporter/content/crashes.js index b1ad0b6bc672..edfbc2464ace 100644 --- a/toolkit/crashreporter/content/crashes.js +++ b/toolkit/crashreporter/content/crashes.js @@ -2,182 +2,229 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -var reportURL; +let reportURL; ChromeUtils.import("resource://gre/modules/CrashReports.jsm"); ChromeUtils.import("resource://gre/modules/Services.jsm"); ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); ChromeUtils.import("resource://gre/modules/osfile.jsm"); -ChromeUtils.defineModuleGetter(this, "CrashSubmit", - "resource://gre/modules/CrashSubmit.jsm"); +ChromeUtils.defineModuleGetter(this, "CrashSubmit", "resource://gre/modules/CrashSubmit.jsm"); -document.addEventListener("DOMContentLoaded", function() { - populateReportList(); - document.getElementById("clear-reports").addEventListener("click", function() { - clearReports().then(null, Cu.reportError); +document.addEventListener("DOMContentLoaded", () => { + populateReportLists(); + document.getElementById("clearUnsubmittedReports").addEventListener("click", () => { + clearUnsubmittedReports().catch(Cu.reportError); + }); + document.getElementById("clearSubmittedReports").addEventListener("click", () => { + clearSubmittedReports().catch(Cu.reportError); }); }); const buildID = Services.appinfo.appBuildID; -function submitPendingReport(event) { - let link = event.target; - let id = link.firstChild.textContent; - link.className = "submitting"; - CrashSubmit.submit(id, { noThrottle: true }).then( - (remoteCrashID) => { - link.className = ""; - // Reset the link to point at our new crash report. This way, if the - // user clicks "Back", the link will be correct. - link.firstChild.textContent = remoteCrashID; - link.setAttribute("id", remoteCrashID); - link.removeEventListener("click", submitPendingReport, true); - - if (reportURL) { - link.setAttribute("href", reportURL + remoteCrashID); - // redirect the user to their brand new crash report - window.location.href = reportURL + remoteCrashID; - } - }, - () => { - // XXX: do something more useful here - link.className = ""; - - // Dispatch an event, useful for testing - let event = document.createEvent("Events"); - event.initEvent("CrashSubmitFailed", true, false); - document.dispatchEvent(event); - }); - event.preventDefault(); - return false; -} - -function populateReportList() { +/** + * Adds the crash reports with submission buttons and links + * to the unsubmitted and submitted crash report lists. + * If breakpad.reportURL is not set, displays a misconfiguration message + * instead. + */ +function populateReportLists() { try { reportURL = Services.prefs.getCharPref("breakpad.reportURL"); // Ignore any non http/https urls - if (!/^https?:/i.test(reportURL)) + if (!/^https?:/i.test(reportURL)) { reportURL = null; - } catch (e) { } - if (!reportURL) { - document.getElementById("clear-reports").style.display = "none"; - document.getElementById("reportList").style.display = "none"; - document.getElementById("noConfig").style.display = "block"; - return; - } - let reports = CrashReports.getReports(); - - if (reports.length == 0) { - document.getElementById("clear-reports").style.display = "none"; - document.getElementById("reportList").style.display = "none"; - document.getElementById("noReports").style.display = "block"; - return; - } - - var dateFormatter; - var timeFormatter; - try { - dateFormatter = new Services.intl.DateTimeFormat(undefined, { dateStyle: "short" }); - timeFormatter = new Services.intl.DateTimeFormat(undefined, { timeStyle: "short" }); + } } catch (e) { - // XXX Fallback to be removed once bug 1215247 is complete - // and the Intl API is available on all platforms. - dateFormatter = { - format(date) { - return date.toLocaleDateString(); - } - }; - timeFormatter = { - format(date) { - return date.toLocaleTimeString(); - } - }; + reportURL = null; } - var reportURI = Services.io.newURI(reportURL); - // resolving this URI relative to /report/index - var aboutThrottling = Services.io.newURI("../../about/throttling", null, reportURI); + if (!reportURL) { + document.getElementById("noConfig").classList.remove("hidden"); + return; + } + const reports = CrashReports.getReports(); + const dateFormatter = new Services.intl.DateTimeFormat(undefined, { + timeStyle: "short", + dateStyle: "short" + }); + reports.forEach(report => addReportRow(report.pending, report.id, report.date, dateFormatter)); + showAppropriateSections(); +} - for (var i = 0; i < reports.length; i++) { - var row = document.createElement("tr"); - var cell = document.createElement("td"); - row.appendChild(cell); - var link = document.createElement("a"); - if (reports[i].pending) { - link.setAttribute("href", aboutThrottling.spec); - link.addEventListener("click", submitPendingReport, true); - } else { - link.setAttribute("href", reportURL + reports[i].id); - } - link.setAttribute("id", reports[i].id); - link.classList.add("crashReport"); - link.appendChild(document.createTextNode(reports[i].id)); - cell.appendChild(link); +/** + * Adds a crash report with the appropriate submission button + * or viewing link to the unsubmitted or submitted report list + * based on isPending. + * + * @param {Boolean} isPending whether the crash is up for submission + * @param {String} id the unique id of the crash report + * @param {Date} date either the date of crash or date of submission + * @param {Object} dateFormatter formatter for presenting dates to users + */ +function addReportRow(isPending, id, date, dateFormatter) { + const rowTemplate = document.getElementById("crashReportRow"); + const row = document.importNode(rowTemplate.content, true).querySelector("tr"); + row.id = id; - var date = new Date(reports[i].date); - cell = document.createElement("td"); - cell.appendChild(document.createTextNode(dateFormatter.format(date))); - row.appendChild(cell); - cell = document.createElement("td"); - cell.appendChild(document.createTextNode(timeFormatter.format(date))); - row.appendChild(cell); - if (reports[i].pending) { - document.getElementById("unsubmitted").appendChild(row); - } else { - document.getElementById("submitted").appendChild(row); - } + const cells = row.querySelectorAll("td"); + cells[0].appendChild(document.createTextNode(id)); + cells[1].appendChild(document.createTextNode(dateFormatter.format(date))); + + if (isPending) { + const buttonTemplate = document.getElementById("crashSubmitButton"); + const button = document.importNode(buttonTemplate.content, true).querySelector("button"); + const buttonText = button.querySelector("span"); + button.addEventListener("click", + () => submitPendingReport(id, row, button, buttonText, dateFormatter)); + cells[2].appendChild(button); + document.getElementById("unsubmitted").appendChild(row); + } else { + const linkTemplate = document.getElementById("viewCrashLink"); + const link = document.importNode(linkTemplate.content, true).querySelector("a"); + link.href = `${reportURL}${id}`; + cells[2].appendChild(link); + document.getElementById("submitted").appendChild(row); } } -var clearReports = async function() { +/** + * Shows or hides each of the unsubmitted and submitted report list + * based on whether they contain at least one crash report. + * If hidden, the submitted report list is replaced by a message + * indicating that no crash reports have been submitted. + */ +function showAppropriateSections() { + let hasUnsubmitted = document.getElementById("unsubmitted").childElementCount > 0; + document.getElementById("reportListUnsubmitted").classList.toggle("hidden", !hasUnsubmitted); + + let hasSubmitted = document.getElementById("submitted").childElementCount > 0; + document.getElementById("reportListSubmitted").classList.toggle("hidden", !hasSubmitted); + document.getElementById("noSubmittedReports").classList.toggle("hidden", hasSubmitted); +} + +/** + * Changes the provided button to display a spinner. Then, tries to submit the + * crash report for the provided id. On success, removes the crash report from + * the list of unsubmitted crash reports and adds a new crash report to the list + * of submitted crash reports. On failure, changes the provided button to display + * a red error message. + * + * @param {String} reportId the unique id of the crash report + * @param {HTMLTableRowElement} row the table row of the crash report + * @param {HTMLButtonElement} button the button pressed to start the submission + * @param {HTMLSpanElement} buttonText the text inside the pressed button + * @param {Object} dateFormatter formatter for presenting dates to users + */ +function submitPendingReport(reportId, row, button, buttonText, dateFormatter) { + button.classList.add("submitting"); + CrashSubmit.submit(reportId, { noThrottle: true }).then(remoteCrashID => { + document.getElementById("unsubmitted").removeChild(row); + const report = CrashReports.getReports().filter(report => report.id === remoteCrashID); + addReportRow(false, remoteCrashID, report.date, dateFormatter); + showAppropriateSections(); + dispatchEvent("CrashSubmitSucceeded"); + }, + () => { + button.classList.remove("submitting"); + button.classList.add("failed-to-submit"); + document.l10n.setAttributes(buttonText, "submit-crash-button-failure-label"); + dispatchEvent("CrashSubmitFailed"); + }, + ); +} + +/** + * Deletes unsubmitted and old crash reports from the user's device. + * Then, hides the list of unsubmitted crash reports. + */ +async function clearUnsubmittedReports() { const [title, description] = await document.l10n.formatValues([ {id: "delete-confirm-title"}, - {id: "delete-confirm-description"}, + {id: "delete-unsubmitted-description"}, ]); if (!Services.prompt.confirm(window, title, description)) { return; } - let cleanupFolder = async function(path, filter) { - let iterator = new OS.File.DirectoryIterator(path); - try { - await iterator.forEach(async function(aEntry) { - if (!filter || (await filter(aEntry))) { - await OS.File.remove(aEntry.path); - } - }); - } catch (e) { - if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) { - throw e; - } - } finally { - iterator.close(); - } - }; + await cleanupFolder(CrashReports.pendingDir.path); + await clearOldReports(); + document.getElementById("reportListUnsubmitted").classList.add("hidden"); +} - await cleanupFolder(CrashReports.submittedDir.path, function(aEntry) { - return aEntry.name.startsWith("bp-") && aEntry.name.endsWith(".txt"); - }); +/** + * Deletes submitted and old crash reports from the user's device. + * Then, hides the list of submitted crash reports. + */ +async function clearSubmittedReports() { + const [title, description] = await document.l10n.formatValues([ + {id: "delete-confirm-title"}, + {id: "delete-submitted-description"}, + ]); + if (!Services.prompt.confirm(window, title, description)) { + return; + } - let oneYearAgo = Date.now() - 31586000000; - await cleanupFolder(CrashReports.reportsDir.path, async function(aEntry) { - if (!aEntry.name.startsWith("InstallTime") || - aEntry.name == "InstallTime" + buildID) { + await cleanupFolder( + CrashReports.submittedDir.path, + async entry => entry.name.startsWith("bp-") && entry.name.endsWith(".txt"), + ); + await clearOldReports(); + document.getElementById("reportListSubmitted").classList.add("hidden"); + document.getElementById("noSubmittedReports").classList.remove("hidden"); +} + +/** + * Deletes old crash reports from the user's device. + */ +async function clearOldReports() { + const oneYearAgo = Date.now() - 31586000000; + await cleanupFolder(CrashReports.reportsDir.path, async entry => { + if (!entry.name.startsWith("InstallTime") || entry.name == "InstallTime" + buildID) { return false; } - let date = aEntry.winLastWriteDate; + let date = entry.winLastWriteDate; if (!date) { - let stat = await OS.File.stat(aEntry.path); + const stat = await OS.File.stat(entry.path); date = stat.lastModificationDate; } - - return (date < oneYearAgo); + return date < oneYearAgo; }); +} - await cleanupFolder(CrashReports.pendingDir.path); +/** + * Deletes files from the user's device at the specified path + * that match the provided filter. + * + * @param {String} path the directory location to delete form + * @param {Function} filter function taking in a file entry and + * returning whether to delete the file + */ +async function cleanupFolder(path, filter) { + const iterator = new OS.File.DirectoryIterator(path); + try { + await iterator.forEach(async entry => { + if (!filter || (await filter(entry))) { + await OS.File.remove(entry.path); + } + }); + } catch (e) { + if (!(e instanceof OS.File.Error) || !e.becauseNoSuchFile) { + throw e; + } + } finally { + iterator.close(); + } +} - document.getElementById("clear-reports").style.display = "none"; - document.getElementById("reportList").style.display = "none"; - document.getElementById("noReports").style.display = "block"; -}; +/** + * Dispatches an event with the specified name. + * + * @param {String} name the name of the event + */ +function dispatchEvent(name) { + const event = document.createEvent("Events"); + event.initEvent(name, true, false); + document.dispatchEvent(event); +} diff --git a/toolkit/crashreporter/content/crashes.xhtml b/toolkit/crashreporter/content/crashes.xhtml index a4e91865306d..f98b716969ce 100644 --- a/toolkit/crashreporter/content/crashes.xhtml +++ b/toolkit/crashreporter/content/crashes.xhtml @@ -4,48 +4,69 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - - - - -