feat(xo-web/logs): transform objects UUIDs into clickable links (#7300)
In Settings/Logs modals : transform objects UUIDs into clickable links, leading to the corresponding object page. For objects that are not found, UUID can be copied to clipboard.
This commit is contained in:
parent
0c0251082d
commit
8e65ef7dbc
@ -21,6 +21,7 @@
|
|||||||
- [Tags] Add tooltips on `xo:no-bak` and `xo:notify-on-snapshot` tags (PR [#7335](https://github.com/vatesfr/xen-orchestra/pull/7335))
|
- [Tags] Add tooltips on `xo:no-bak` and `xo:notify-on-snapshot` tags (PR [#7335](https://github.com/vatesfr/xen-orchestra/pull/7335))
|
||||||
- [VM] Custom notes [#5792](https://github.com/vatesfr/xen-orchestra/issues/5792) (PR [#7322](https://github.com/vatesfr/xen-orchestra/pull/7322))
|
- [VM] Custom notes [#5792](https://github.com/vatesfr/xen-orchestra/issues/5792) (PR [#7322](https://github.com/vatesfr/xen-orchestra/pull/7322))
|
||||||
- [Pool/Advanced] Ability to do a `Rolling Pool Reboot` (Enterprise plans) [#6885](https://github.com/vatesfr/xen-orchestra/issues/6885) (PRs [#7243](https://github.com/vatesfr/xen-orchestra/pull/7243), [#7242](https://github.com/vatesfr/xen-orchestra/pull/7242))
|
- [Pool/Advanced] Ability to do a `Rolling Pool Reboot` (Enterprise plans) [#6885](https://github.com/vatesfr/xen-orchestra/issues/6885) (PRs [#7243](https://github.com/vatesfr/xen-orchestra/pull/7243), [#7242](https://github.com/vatesfr/xen-orchestra/pull/7242))
|
||||||
|
- [Settings/Logs] Transform objects UUIDs and OpaqueRefs into clickable links, leading to the corresponding object page (PR [#7300](https://github.com/vatesfr/xen-orchestra/pull/7300))
|
||||||
|
|
||||||
### Bug fixes
|
### Bug fixes
|
||||||
|
|
||||||
|
@ -7,3 +7,9 @@
|
|||||||
.container:hover .button {
|
.container:hover .button {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.modal-body) .container .button {
|
||||||
|
position: relative;
|
||||||
|
margin-left: -4ex;
|
||||||
|
left: 3.5em;
|
||||||
|
}
|
||||||
|
@ -18,7 +18,6 @@ const Copiable = ({ className, tagName = 'span', ...props }) =>
|
|||||||
className: classNames(styles.container, className),
|
className: classNames(styles.container, className),
|
||||||
},
|
},
|
||||||
props.children,
|
props.children,
|
||||||
' ',
|
|
||||||
<Tooltip content={_('copyToClipboard')}>
|
<Tooltip content={_('copyToClipboard')}>
|
||||||
<CopyToClipboard text={props.data || props.children}>
|
<CopyToClipboard text={props.data || props.children}>
|
||||||
<Button className={styles.button} size='small'>
|
<Button className={styles.button} size='small'>
|
||||||
|
79
packages/xo-web/src/common/rich-text.js
Normal file
79
packages/xo-web/src/common/rich-text.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { connectStore } from 'utils'
|
||||||
|
import { createGetObjectsOfType } from 'selectors'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import decorate from 'apply-decorators'
|
||||||
|
import { injectState, provideState } from 'reaclette'
|
||||||
|
import Copiable from 'copiable'
|
||||||
|
import { flatMap } from 'lodash'
|
||||||
|
import Link from 'link'
|
||||||
|
import store from 'store'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO : check user permissions on objects retrieved by refs, if using the component in non-admin pages
|
||||||
|
*/
|
||||||
|
const RichText = decorate([
|
||||||
|
connectStore({
|
||||||
|
vms: createGetObjectsOfType('VM'),
|
||||||
|
hosts: createGetObjectsOfType('host'),
|
||||||
|
pools: createGetObjectsOfType('pool'),
|
||||||
|
srs: createGetObjectsOfType('SR'),
|
||||||
|
}),
|
||||||
|
provideState({
|
||||||
|
computed: {
|
||||||
|
idToLink: (_, props) => {
|
||||||
|
const regex = /\b(?:OpaqueRef:)?[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\b/g
|
||||||
|
const parts = props.message.split(regex)
|
||||||
|
const ids = props.message.match(regex) || []
|
||||||
|
const { objects } = store.getState()
|
||||||
|
|
||||||
|
return flatMap(parts, (part, index) => {
|
||||||
|
// If on last part, return only the part without adding Copiable component
|
||||||
|
if (index === ids.length) {
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ids[index]
|
||||||
|
let _object
|
||||||
|
|
||||||
|
for (const collection of [props.vms, props.hosts, props.pools, props.srs]) {
|
||||||
|
_object = id.startsWith('OpaqueRef:') ? objects.byRef.get(id) : collection[id]
|
||||||
|
|
||||||
|
if (_object !== undefined) break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_object !== undefined && ['VM', 'host', 'pool', 'SR'].includes(_object.type)) {
|
||||||
|
return [
|
||||||
|
part,
|
||||||
|
<Link key={index} to={`/${_object.type.toLowerCase()}s/${_object.uuid}`}>
|
||||||
|
{id}
|
||||||
|
</Link>,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return [part, <Copiable key={index}>{id}</Copiable>]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
injectState,
|
||||||
|
({ state: { idToLink }, copiable, message }) =>
|
||||||
|
copiable ? (
|
||||||
|
<Copiable tagName='pre' data={message}>
|
||||||
|
{idToLink}
|
||||||
|
</Copiable>
|
||||||
|
) : (
|
||||||
|
<pre>{idToLink}</pre>
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
RichText.propTypes = {
|
||||||
|
message: PropTypes.string,
|
||||||
|
copiable: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
RichText.defaultProps = {
|
||||||
|
copiable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RichText
|
@ -119,11 +119,12 @@ export default {
|
|||||||
objects: combineActionHandlers(
|
objects: combineActionHandlers(
|
||||||
{
|
{
|
||||||
all: {}, // Mutable for performance!
|
all: {}, // Mutable for performance!
|
||||||
|
byRef: new Map(), // Mutable for performance!
|
||||||
byType: {},
|
byType: {},
|
||||||
fetched: false,
|
fetched: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[actions.updateObjects]: ({ all, byType: prevByType, fetched }, updates) => {
|
[actions.updateObjects]: ({ all, byRef, byType: prevByType, fetched }, updates) => {
|
||||||
const byType = { ...prevByType }
|
const byType = { ...prevByType }
|
||||||
const get = type => {
|
const get = type => {
|
||||||
const curr = byType[type]
|
const curr = byType[type]
|
||||||
@ -139,6 +140,7 @@ export default {
|
|||||||
const { type } = object
|
const { type } = object
|
||||||
|
|
||||||
all[id] = object
|
all[id] = object
|
||||||
|
byRef.set(object._xapiRef, object)
|
||||||
get(type)[id] = object
|
get(type)[id] = object
|
||||||
|
|
||||||
if (previous && previous.type !== type) {
|
if (previous && previous.type !== type) {
|
||||||
@ -147,10 +149,11 @@ export default {
|
|||||||
} else if (previous) {
|
} else if (previous) {
|
||||||
delete all[id]
|
delete all[id]
|
||||||
delete get(previous.type)[id]
|
delete get(previous.type)[id]
|
||||||
|
byRef.delete(previous._xapiRef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { all, byType, fetched }
|
return { all, byRef, byType, fetched }
|
||||||
},
|
},
|
||||||
[actions.markObjectsFetched]: state => ({
|
[actions.markObjectsFetched]: state => ({
|
||||||
...state,
|
...state,
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
generateAuditFingerprint,
|
generateAuditFingerprint,
|
||||||
getPlugin,
|
getPlugin,
|
||||||
} from 'xo'
|
} from 'xo'
|
||||||
|
import RichText from 'rich-text'
|
||||||
|
|
||||||
const getIntegrityErrorRender = ({ nValid, error }) => (
|
const getIntegrityErrorRender = ({ nValid, error }) => (
|
||||||
<p className='text-danger'>
|
<p className='text-danger'>
|
||||||
@ -166,7 +167,7 @@ const displayRecord = record =>
|
|||||||
<span>
|
<span>
|
||||||
<Icon icon='audit' /> {_('auditRecord')}
|
<Icon icon='audit' /> {_('auditRecord')}
|
||||||
</span>,
|
</span>,
|
||||||
<Copiable tagName='pre'>{JSON.stringify(record, null, 2)}</Copiable>
|
<RichText copiable message={JSON.stringify(record, null, 2)} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const INDIVIDUAL_ACTIONS = [
|
const INDIVIDUAL_ACTIONS = [
|
||||||
|
@ -4,7 +4,6 @@ import { find, map } from 'lodash'
|
|||||||
|
|
||||||
import _ from 'intl'
|
import _ from 'intl'
|
||||||
import BaseComponent from 'base-component'
|
import BaseComponent from 'base-component'
|
||||||
import Copiable from 'copiable'
|
|
||||||
import NoObjects from 'no-objects'
|
import NoObjects from 'no-objects'
|
||||||
import SortedTable from 'sorted-table'
|
import SortedTable from 'sorted-table'
|
||||||
import styles from './index.css'
|
import styles from './index.css'
|
||||||
@ -14,6 +13,7 @@ import { createSelector } from 'selectors'
|
|||||||
import { get } from '@xen-orchestra/defined'
|
import { get } from '@xen-orchestra/defined'
|
||||||
import { reportBug } from 'report-bug-button'
|
import { reportBug } from 'report-bug-button'
|
||||||
import { deleteApiLog, deleteApiLogs, subscribeApiLogs, subscribeUsers } from 'xo'
|
import { deleteApiLog, deleteApiLogs, subscribeApiLogs, subscribeUsers } from 'xo'
|
||||||
|
import RichText from 'rich-text'
|
||||||
|
|
||||||
const formatMessage = data =>
|
const formatMessage = data =>
|
||||||
`\`\`\`\n${data.method}\n${JSON.stringify(data.params, null, 2)}\n${JSON.stringify(data.error, null, 2).replace(
|
`\`\`\`\n${data.method}\n${JSON.stringify(data.params, null, 2)}\n${JSON.stringify(data.error, null, 2).replace(
|
||||||
@ -99,7 +99,7 @@ const ACTIONS = [
|
|||||||
|
|
||||||
const INDIVIDUAL_ACTIONS = [
|
const INDIVIDUAL_ACTIONS = [
|
||||||
{
|
{
|
||||||
handler: log => alert(_('logError'), <Copiable tagName='pre'>{formatLog(log)}</Copiable>),
|
handler: log => alert(_('logError'), <RichText copiable message={formatLog(log)} />),
|
||||||
icon: 'preview',
|
icon: 'preview',
|
||||||
label: _('logDisplayDetails'),
|
label: _('logDisplayDetails'),
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user