diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index a253272bc..a4a4226ad 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -6,6 +6,7 @@ - [Home/VM] Sort VM by start time [#3955](https://github.com/vatesfr/xen-orchestra/issues/3955) (PR [#3970](https://github.com/vatesfr/xen-orchestra/pull/3970)) - [Editable fields] Unfocusing (clicking outside) submits the change instead of canceling (PR [#3980](https://github.com/vatesfr/xen-orchestra/pull/3980)) - [Network] Dedicated page for network creation [#3895](https://github.com/vatesfr/xen-orchestra/issues/3895) (PR [#3906](https://github.com/vatesfr/xen-orchestra/pull/3906)) +- [Logs] Add button to download the log [#3957](https://github.com/vatesfr/xen-orchestra/issues/3957) (PR [#3985](https://github.com/vatesfr/xen-orchestra/pull/3985)) ### Bug fixes diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index db5cea6ba..ac46bc500 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -1874,6 +1874,7 @@ const messages = { logError: 'Error', logTitle: 'Logs', logDisplayDetails: 'Display details', + logDownload: 'Download log', logTime: 'Date', logNoStackTrace: 'No stack trace', logNoParams: 'No params', diff --git a/packages/xo-web/src/common/utils.js b/packages/xo-web/src/common/utils.js index 2eb5b9196..87a454e52 100644 --- a/packages/xo-web/src/common/utils.js +++ b/packages/xo-web/src/common/utils.js @@ -601,3 +601,20 @@ export const getIscsiPaths = pbd => { const pathsInfo = pbd.otherConfig[`mpath-${pbd.device_config.SCSIid}`] return pathsInfo !== undefined ? JSON.parse(pathsInfo) : [] } + +// =================================================================== + +export const downloadLog = ({ log, date, type }) => { + const file = new window.Blob([log], { + type: 'text/plain', + }) + const anchor = document.createElement('a') + anchor.href = window.URL.createObjectURL(file) + anchor.download = `${new Date(date) + .toISOString() + .replace(/:/g, '_')} - ${type}.log` + anchor.style.display = 'none' + document.body.appendChild(anchor) + anchor.click() + document.body.removeChild(anchor) +} diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss index a74270840..4736107d3 100644 --- a/packages/xo-web/src/icons.scss +++ b/packages/xo-web/src/icons.scss @@ -108,6 +108,10 @@ @extend .fa; @extend .fa-clipboard; } + &-download { + @extend .fa; + @extend .fa-download; + } &-shortcuts { @extend .fa; @extend .fa-keyboard-o; @@ -489,7 +493,8 @@ } // SR - &-sr, &-vdi { + &-sr, + &-vdi { &-reconnect-all { @extend .fa; @extend .fa-retweet; @@ -563,7 +568,8 @@ } // Host and VM actions - &-host, &-vm { + &-host, + &-vm { &-start { @extend .fa; @extend .fa-play; @@ -611,12 +617,12 @@ &-filters { @extend .fa; - @extend .fa-filter + @extend .fa-filter; } &-tags { @extend .fa; - @extend .fa-tags + @extend .fa-tags; } &-remove-tag { @@ -656,11 +662,11 @@ } &-minus { @extend .fa; - @extend .fa-minus + @extend .fa-minus; } &-plus { @extend .fa; - @extend .fa-plus + @extend .fa-plus; } &-clear-search { @extend .fa; @@ -871,7 +877,7 @@ &-new-vm { &-infos { @extend .fa; - @extend .fa-info-circle + @extend .fa-info-circle; } &-perf { @extend .fa; @@ -990,7 +996,7 @@ color: #ccc; } - // About + // About &-bug { @extend .fa; @extend .fa-bug; diff --git a/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-header.js b/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-header.js index c64a82a35..d4fd131f5 100644 --- a/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-header.js +++ b/packages/xo-web/src/xo-app/logs/backup-ng/log-alert-header.js @@ -9,6 +9,7 @@ import Icon from 'icon' import React from 'react' import ReportBugButton, { CAN_REPORT_BUG } from 'report-bug-button' import Tooltip from 'tooltip' +import { downloadLog } from 'utils' import { get } from '@xen-orchestra/defined' import { injectState, provideState } from 'reaclette' import { keyBy } from 'lodash' @@ -28,6 +29,8 @@ export default decorate([ })), provideState({ effects: { + _downloadLog: () => ({ formattedLog }, { log }) => + downloadLog({ log: formattedLog, date: log.start, type: 'backup NG' }), restartFailedVms: () => async ( _, { log: { jobId: id, scheduleId: schedule, tasks, infos } } @@ -81,6 +84,11 @@ export default decorate([ + + + {CAN_REPORT_BUG && ( 2 )}\n${JSON.stringify(data.error, null, 2).replace(/\\n/g, '\n')}\n\`\`\`` +const formatLog = log => + `${log.data.method}\n${JSON.stringify( + log.data.params, + null, + 2 + )}\n${JSON.stringify(log.data.error, null, 2).replace(/\\n/g, '\n')}` + const COLUMNS = [ { name: _('logUser'), @@ -87,19 +94,16 @@ const ACTIONS = [ const INDIVIDUAL_ACTIONS = [ { handler: log => - alert( - _('logError'), - - {`${log.data.method}\n${JSON.stringify( - log.data.params, - null, - 2 - )}\n${JSON.stringify(log.data.error, null, 2).replace(/\\n/g, '\n')}`} - - ), + alert(_('logError'), {formatLog(log)}), icon: 'preview', label: _('logDisplayDetails'), }, + { + handler: log => + downloadLog({ log: formatLog(log), date: log.time, type: 'XO' }), + icon: 'download', + label: _('logDownload'), + }, { disabled: !CAN_REPORT_BUG, handler: log =>