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:
OlivierFL 2024-01-26 17:28:30 +01:00 committed by GitHub
parent 0c0251082d
commit 8e65ef7dbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 95 additions and 6 deletions

View File

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

View File

@ -7,3 +7,9 @@
.container:hover .button {
visibility: visible;
}
:global(.modal-body) .container .button {
position: relative;
margin-left: -4ex;
left: 3.5em;
}

View File

@ -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'>

View 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

View File

@ -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,

View File

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

View File

@ -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'),
},