feat(logs/backup-ng): support ticket with attached log (#4201)
Related to support-panel#24
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
7
packages/xo-web/src/common/xoa-plans.js
Normal file
7
packages/xo-web/src/common/xoa-plans.js
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user