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))
|
||||
- [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))
|
||||
- [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
|
||||
|
||||
|
@ -7,3 +7,9 @@
|
||||
.container:hover .button {
|
||||
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),
|
||||
},
|
||||
props.children,
|
||||
' ',
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<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(
|
||||
{
|
||||
all: {}, // Mutable for performance!
|
||||
byRef: new Map(), // Mutable for performance!
|
||||
byType: {},
|
||||
fetched: false,
|
||||
},
|
||||
{
|
||||
[actions.updateObjects]: ({ all, byType: prevByType, fetched }, updates) => {
|
||||
[actions.updateObjects]: ({ all, byRef, byType: prevByType, fetched }, updates) => {
|
||||
const byType = { ...prevByType }
|
||||
const get = type => {
|
||||
const curr = byType[type]
|
||||
@ -139,6 +140,7 @@ export default {
|
||||
const { type } = object
|
||||
|
||||
all[id] = object
|
||||
byRef.set(object._xapiRef, object)
|
||||
get(type)[id] = object
|
||||
|
||||
if (previous && previous.type !== type) {
|
||||
@ -147,10 +149,11 @@ export default {
|
||||
} else if (previous) {
|
||||
delete all[id]
|
||||
delete get(previous.type)[id]
|
||||
byRef.delete(previous._xapiRef)
|
||||
}
|
||||
}
|
||||
|
||||
return { all, byType, fetched }
|
||||
return { all, byRef, byType, fetched }
|
||||
},
|
||||
[actions.markObjectsFetched]: state => ({
|
||||
...state,
|
||||
|
@ -26,6 +26,7 @@ import {
|
||||
generateAuditFingerprint,
|
||||
getPlugin,
|
||||
} from 'xo'
|
||||
import RichText from 'rich-text'
|
||||
|
||||
const getIntegrityErrorRender = ({ nValid, error }) => (
|
||||
<p className='text-danger'>
|
||||
@ -166,7 +167,7 @@ const displayRecord = record =>
|
||||
<span>
|
||||
<Icon icon='audit' /> {_('auditRecord')}
|
||||
</span>,
|
||||
<Copiable tagName='pre'>{JSON.stringify(record, null, 2)}</Copiable>
|
||||
<RichText copiable message={JSON.stringify(record, null, 2)} />
|
||||
)
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
|
@ -4,7 +4,6 @@ import { find, map } from 'lodash'
|
||||
|
||||
import _ from 'intl'
|
||||
import BaseComponent from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import NoObjects from 'no-objects'
|
||||
import SortedTable from 'sorted-table'
|
||||
import styles from './index.css'
|
||||
@ -14,6 +13,7 @@ import { createSelector } from 'selectors'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { reportBug } from 'report-bug-button'
|
||||
import { deleteApiLog, deleteApiLogs, subscribeApiLogs, subscribeUsers } from 'xo'
|
||||
import RichText from 'rich-text'
|
||||
|
||||
const formatMessage = data =>
|
||||
`\`\`\`\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 = [
|
||||
{
|
||||
handler: log => alert(_('logError'), <Copiable tagName='pre'>{formatLog(log)}</Copiable>),
|
||||
handler: log => alert(_('logError'), <RichText copiable message={formatLog(log)} />),
|
||||
icon: 'preview',
|
||||
label: _('logDisplayDetails'),
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user