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 =>