feat(logs/backup-ng): support ticket with attached log (#4201)

Related to support-panel#24
This commit is contained in:
badrAZ
2019-11-13 15:57:52 +01:00
committed by Julien Fontanet
parent 47bb79cce1
commit dab1788a3b
8 changed files with 168 additions and 45 deletions

View File

@@ -7,6 +7,7 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Logs] Ability to report a bug with attached log (PR [#4201](https://github.com/vatesfr/xen-orchestra/pull/4201))
### Bug fixes

View File

@@ -34,6 +34,7 @@
"@nraynaud/novnc": "0.6.1",
"@xen-orchestra/cron": "^1.0.6",
"@xen-orchestra/defined": "^0.0.0",
"@xen-orchestra/log": "^0.2.0",
"@xen-orchestra/template": "^0.1.0",
"ansi_up": "^4.0.3",
"asap": "^2.0.6",
@@ -130,6 +131,7 @@
"reselect": "^2.5.4",
"rimraf": "^3.0.0",
"semver": "^6.0.0",
"strip-ansi": "^5.2.0",
"styled-components": "^3.1.5",
"uglify-es": "^3.3.4",
"uncontrollable-input": "^0.1.1",

View File

@@ -1,13 +1,26 @@
import _ from 'intl'
import * as xoaPlans from 'xoa-plans'
import decorate from 'apply-decorators'
import PropTypes from 'prop-types'
import React from 'react'
import stripAnsi from 'strip-ansi'
import xoaUpdater from 'xoa-updater'
import { checkXoa } from 'xo'
import { createBlobFromString } from 'utils'
import { createLogger } from '@xen-orchestra/log'
import { identity, omit } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { post } from 'fetch'
import { timeout } from 'promise-toolbox'
import ActionButton from './action-button'
import ActionRowButton from './action-row-button'
export const CAN_REPORT_BUG = __DEV__ && process.env.XOA_PLAN > 1
const logger = createLogger('report-bug-button')
export const reportBug = ({ formatMessage, message, title }) => {
const SUPPORT_PANEL_URL = './api/support/create/ticket'
const reportOnGithub = ({ formatMessage, message, title }) => {
const encodedTitle = encodeURIComponent(title == null ? '' : title)
const encodedMessage = encodeURIComponent(
message == null
@@ -18,38 +31,111 @@ export const reportBug = ({ formatMessage, message, title }) => {
)
window.open(
process.env.XOA_PLAN < 5
? `https://xen-orchestra.com/#!/member/support?title=${encodedTitle}&message=${encodedMessage}`
: `https://github.com/vatesfr/xen-orchestra/issues/new?title=${encodedTitle}&body=${encodedMessage}`
`https://github.com/vatesfr/xen-orchestra/issues/new?title=${encodedTitle}&body=${encodedMessage}`
)
}
const ReportBugButton = ({
formatMessage,
const reportOnSupportPanel = async ({
files = [],
formatMessage = identity,
message,
rowButton,
title,
...props
}) => {
const Button = rowButton ? ActionRowButton : ActionButton
return (
<Button
{...props}
data-formatMessage={formatMessage}
data-message={message}
data-title={title}
handler={reportBug}
const { FormData, open } = window
const formData = new FormData()
if (title !== undefined) {
formData.append('title', title)
}
if (message !== undefined) {
formData.append('message', formatMessage(message))
}
files.forEach(({ content, name }) => {
formData.append('attachments', content, name)
})
await Promise.all([
timeout
.call(xoaUpdater.getLocalManifest(), 5e3)
.then(
manifest =>
formData.append(
'attachments',
createBlobFromString(JSON.stringify(manifest, null, 2)),
'manifest.json'
),
error => logger.warn('cannot get the local manifest', { error })
),
timeout
.call(checkXoa(), 5e3)
.then(
xoaCheck =>
formData.append(
'attachments',
createBlobFromString(stripAnsi(xoaCheck)),
'xoaCheck.txt'
),
error => logger.warn('cannot get the xoa check', { error })
),
])
const res = await post(SUPPORT_PANEL_URL, formData)
if (res.status !== 200) {
throw new Error('cannot get the new ticket URL')
}
open(await res.text())
}
export const reportBug =
xoaPlans.CURRENT === xoaPlans.SOURCES ? reportOnGithub : reportOnSupportPanel
const REPORT_BUG_BUTTON_PROP_TYPES = {
files: PropTypes.arrayOf(
PropTypes.shape({
content: PropTypes.oneOfType([
PropTypes.instanceOf(window.Blob),
PropTypes.instanceOf(window.File),
]).isRequired,
name: PropTypes.string.isRequired,
})
),
formatMessage: PropTypes.func,
message: PropTypes.string,
rowButton: PropTypes.bool,
title: PropTypes.string,
}
const ReportBugButton = decorate([
provideState({
effects: {
async reportBug() {
const { files, formatMessage, message, title } = this.props
await reportBug({
files,
formatMessage,
message,
title,
})
},
},
computed: {
Button: (state, { rowButton }) =>
rowButton ? ActionRowButton : ActionButton,
buttonProps: (state, props) =>
omit(props, Object.keys(REPORT_BUG_BUTTON_PROP_TYPES)),
},
}),
injectState,
({ state, effects }) => (
<state.Button
{...state.buttonProps}
handler={effects.reportBug}
icon='bug'
tooltip={_('reportBug')}
/>
)
}
),
])
ReportBugButton.propTypes = {
formatMessage: PropTypes.func,
message: PropTypes.string.isRequired,
rowButton: PropTypes.bool,
title: PropTypes.string.isRequired,
}
ReportBugButton.propTypes = REPORT_BUG_BUTTON_PROP_TYPES
export { ReportBugButton as default }

View File

@@ -550,15 +550,24 @@ export const getIscsiPaths = pbd => {
// ===================================================================
export const downloadLog = ({ log, date, type }) => {
const file = new window.Blob([log], {
export const createBlobFromString = str =>
new window.Blob([str], {
type: 'text/plain',
})
// ===================================================================
// Format a date in ISO 8601 in a safe way to be used in filenames
// (even on Windows).
export const safeDateFormat = ms =>
new Date(ms).toISOString().replace(/:/g, '_')
// ===================================================================
export const downloadLog = ({ log, date, type }) => {
const anchor = document.createElement('a')
anchor.href = window.URL.createObjectURL(file)
anchor.download = `${new Date(date)
.toISOString()
.replace(/:/g, '_')} - ${type}.log`
anchor.href = window.URL.createObjectURL(createBlobFromString(log))
anchor.download = `${safeDateFormat(date)} - ${type}.log`
anchor.style.display = 'none'
document.body.appendChild(anchor)
anchor.click()

View File

@@ -0,0 +1,7 @@
export const CURRENT = +process.env.XOA_PLAN
export const FREE = 1
export const STARTER = 2
export const ENTERPRISE = 3
export const PREMIUM = 4
export const SOURCES = 5

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import * as xoaPlans from 'xoa-plans'
import ActionButton from 'action-button'
import addSubscriptions from 'add-subscriptions'
import Button from 'button'
@@ -7,10 +8,10 @@ import CopyToClipboard from 'react-copy-to-clipboard'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import ReportBugButton, { CAN_REPORT_BUG } from 'report-bug-button'
import ReportBugButton from 'report-bug-button'
import Tooltip from 'tooltip'
import { downloadLog } from 'utils'
import { get } from '@xen-orchestra/defined'
import { createBlobFromString, downloadLog, safeDateFormat } from 'utils'
import { get, ifDef } from '@xen-orchestra/defined'
import { injectState, provideState } from 'reaclette'
import { keyBy } from 'lodash'
import {
@@ -66,6 +67,31 @@ export default decorate([
formattedLog: (_, { log }) => JSON.stringify(log, null, 2),
jobFailed: (_, { log = {} }) =>
log.status !== 'success' && log.status !== 'pending',
reportBugProps: ({ formattedLog }, { log = {}, jobs = {} }) => {
const props = {
size: 'small',
title: 'Backup job failed',
}
if (xoaPlans.CURRENT === xoaPlans.SOURCES) {
props.message = `\`\`\`json\n${formattedLog}\n\`\`\``
} else {
const formattedDate = ifDef(log.start, safeDateFormat)
props.files = [
{
content: createBlobFromString(formattedLog),
name: `${formattedDate} - log.json`,
},
]
const job = jobs[log.jobId]
if (job !== undefined) {
props.files.push({
content: createBlobFromString(JSON.stringify(job, null, 2)),
name: 'job.json',
})
}
}
return props
},
},
}),
injectState,
@@ -89,13 +115,7 @@ export default decorate([
<Icon icon='download' />
</Button>
</Tooltip>
{CAN_REPORT_BUG && (
<ReportBugButton
message={`\`\`\`json\n${state.formattedLog}\n\`\`\``}
size='small'
title='Backup job failed'
/>
)}
<ReportBugButton {...state.reportBugProps} />
{state.jobFailed && log.scheduleId !== undefined && (
<ButtonGroup>
<ActionButton

View File

@@ -11,8 +11,8 @@ import styles from './index.css'
import { addSubscriptions, downloadLog } from 'utils'
import { alert } from 'modal'
import { createSelector } from 'selectors'
import { CAN_REPORT_BUG, reportBug } from 'report-bug-button'
import { get } from '@xen-orchestra/defined'
import { reportBug } from 'report-bug-button'
import {
deleteApiLog,
deleteApiLogs,
@@ -118,7 +118,6 @@ const INDIVIDUAL_ACTIONS = [
label: _('logDownload'),
},
{
disabled: !CAN_REPORT_BUG,
handler: log =>
reportBug({
formatMessage,

View File

@@ -8,7 +8,7 @@ import React from 'react'
import SortedTable from 'sorted-table'
import { addSubscriptions } from 'utils'
import { alert } from 'modal'
import { CAN_REPORT_BUG, reportBug } from 'report-bug-button'
import { reportBug } from 'report-bug-button'
import { filter, some } from 'lodash'
import { FormattedDate } from 'react-intl'
import { injectState, provideState } from 'reaclette'
@@ -69,7 +69,6 @@ const COLUMNS = [
const ACTIONS = [
{
disabled: !CAN_REPORT_BUG,
label: _('messageReply'),
handler: notification =>
reportBug({