Compare commits

..

2 Commits

Author SHA1 Message Date
Florent Beauchamp
d8c3ad9294 feat(xo-web/tags): implement scoped tags 2023-12-22 11:04:06 +00:00
Julien Fontanet
0d4cf48410 feat(xo-cli rest): explicit error if not registered
Fixes https://xcp-ng.org/forum/post/68698
2023-12-22 11:33:08 +01:00
5 changed files with 100 additions and 48 deletions

View File

@@ -11,6 +11,7 @@
- [VDI/Export] Expose NBD settings in the XO and REST APIs api (PR [#7251](https://github.com/vatesfr/xen-orchestra/pull/7251))
- [Menu/Proxies] Added a warning icon if unable to check proxies upgrade (PR [#7237](https://github.com/vatesfr/xen-orchestra/pull/7237))
- [Tags] Implement scoped tags (PR [#7258](https://github.com/vatesfr/xen-orchestra/pull/7258))
### Bug fixes
@@ -35,6 +36,7 @@
<!--packages-start-->
- vhd-lib patch
- xo-cli minor
- xo-server minor
- xo-web minor

View File

@@ -157,32 +157,33 @@ function extractFlags(args) {
const noop = Function.prototype
function parseValue(value) {
if (value.startsWith('json:')) {
return JSON.parse(value.slice(5))
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
return value
}
const PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters(args) {
if (args[0] === '--') {
return args.slice(1).map(parseValue)
}
const params = {}
forEach(args, function (arg) {
let matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
}
params[matches[1]] = parseValue(matches[2])
const name = matches[1]
let value = matches[2]
if (value.startsWith('json:')) {
value = JSON.parse(value.slice(5))
}
if (name === '@') {
params['@'] = value
return
}
if (value === 'true') {
value = true
} else if (value === 'false') {
value = false
}
params[name] = value
})
return params

View File

@@ -134,6 +134,12 @@ export async function rest(args) {
const { allowUnauthorized, server, token } = await config.load()
if (server === undefined) {
const errorMessage =
'Please use `xo-cli --register` to associate with an XO instance first.\n\nSee `xo-cli --help` for more info.'
throw errorMessage
}
const baseUrl = server
const baseOpts = {
headers: {

View File

@@ -251,19 +251,9 @@ export default class Api {
constructor(app) {
this._logger = null
this._methods = { __proto__: null }
this._app = app
const defer =
const seq = async methods => {
for (const method of methods) {
await this.#callApiMethod(method[0], method[1])
}
}
seq.validate = ajv.compile({ type: 'array', minLength: 1, items: { type: ['array', 'string'] } })
const if =
this._methods = { __proto__: null, seq }
this.addApiMethods(methods)
app.hooks.on('start', async () => {
this._logger = await app.getLogger('api')
@@ -377,7 +367,8 @@ export default class Api {
}
async callApiMethod(connection, name, params = {}) {
if (!Object.hasOwn(this._methods, name)) {
const method = this._methods[name]
if (!method) {
throw new MethodNotFound(name)
}
@@ -392,12 +383,11 @@ export default class Api {
apiContext.permission = 'none'
}
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, params))
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, method, params))
}
async #callApiMethod(name, params) {
async #callApiMethod(name, method, params) {
const app = this._app
const method = this._methods[name]
const startTime = Date.now()
const { connection, user } = this.apiContext

View File

@@ -17,18 +17,23 @@ const INPUT_STYLE = {
maxWidth: '8em',
}
const TAG_STYLE = {
backgroundColor: '#2598d9',
borderRadius: '0.5em',
border: '0.2em solid #2598d9',
backgroundColor: '#2598d9',
color: 'white',
fontSize: '0.6em',
margin: '0.2em',
marginTop: '-0.1em',
padding: '0.3em',
verticalAlign: 'middle',
}
const LINK_STYLE = {
cursor: 'pointer',
}
const TAG_STYLE_ONCLICK = {
...TAG_STYLE,
...LINK_STYLE,
}
const ADD_TAG_STYLE = {
cursor: 'pointer',
fontSize: '0.8em',
@@ -38,6 +43,23 @@ const REMOVE_TAG_STYLE = {
cursor: 'pointer',
}
const TAG_SCOPE_STYLE = {
padding: '0.1em',
borderRadius: '0.2em',
}
const TAG_SCOPE_LINK_STYLE = {
...TAG_SCOPE_STYLE,
...LINK_STYLE,
}
const TAG_SCOPE_VALUE_STYLE = {
padding: '0 0.2em',
color: '#2598d9',
backgroundColor: 'white',
borderTopRightRadius: '0.3em', // tag border radius - tag padding
borderBottomRightRadius: '0.3em', // tag border radius - tag padding
}
class SelectExistingTag extends Component {
state = { tags: [] }
@@ -156,20 +178,51 @@ export default class Tags extends Component {
}
}
export const Tag = ({ type, label, onDelete, onClick }) => (
<span style={TAG_STYLE}>
<span onClick={onClick && (() => onClick(label))} style={onClick && LINK_STYLE}>
{label}
</span>{' '}
{onDelete ? (
<span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
<Icon icon='remove-tag' />
const ScopedTag = ({ type, label, scope, value, onDelete, onClick }) => {
return (
<span style={onClick ? TAG_STYLE_ONCLICK : TAG_STYLE}>
<span style={onClick ? TAG_SCOPE_LINK_STYLE : TAG_SCOPE_STYLE} onClick={onClick && (() => onClick(label))}>
{scope}
</span>
) : (
[]
)}
</span>
)
<span style={TAG_SCOPE_VALUE_STYLE}>
<span onClick={onClick && (() => onClick(label))} style={onClick && LINK_STYLE}>
{value}
</span>{' '}
{onDelete && (
<span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
<Icon icon='remove-tag' />
</span>
)}
</span>
</span>
)
}
ScopedTag.propTypes = {
scope: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
}
export const Tag = ({ type, label, onDelete, onClick }) => {
const [scope, ...values] = label.split('=')
if (scope && values?.length > 0) {
return <ScopedTag {...{ type, label, scope, value: values.join('='), onDelete, onClick }} />
}
return (
<span style={TAG_STYLE}>
<span onClick={onClick && (() => onClick(label))} style={onClick && LINK_STYLE}>
{label}
</span>{' '}
{onDelete ? (
<span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
<Icon icon='remove-tag' />
</span>
) : (
[]
)}
</span>
)
}
Tag.propTypes = {
label: PropTypes.string.isRequired,
}