feat(xo-web): scoped tags (#7270)

Based on #7258 developed by @fbeauchamp.

- use inline blocks to respect all paddings/margins
- main settings are in easily modifiable variables
- text color is either black or white based on the background color luminance
- make sure tags and surrounding action buttons are aligned
- always display value in black on white
- delete button use tag color if dark, otherwise black
- Tag component accept color param
This commit is contained in:
Julien Fontanet
2023-12-28 23:10:35 +01:00
committed by GitHub
parent 0d127f2b92
commit 2c40b99d8b
9 changed files with 130 additions and 49 deletions

View File

@@ -7,6 +7,8 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [Tags] Implement scoped tags (PR [#7270](https://github.com/vatesfr/xen-orchestra/pull/7270))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -27,4 +29,6 @@
<!--packages-start-->
- xo-web minor
<!--packages-end-->

View File

@@ -120,6 +120,7 @@
"readable-stream": "^3.0.2",
"redux": "^4.0.0",
"redux-thunk": "^2.0.1",
"relative-luminance": "^2.0.1",
"reselect": "^2.5.4",
"rimraf": "^5.0.1",
"sass": "^1.38.1",

View File

@@ -5,6 +5,7 @@ import map from 'lodash/map'
import pFinally from 'promise-toolbox/finally'
import PropTypes from 'prop-types'
import React from 'react'
import relativeLuminance from 'relative-luminance'
import ActionButton from './action-button'
import Component from './base-component'
@@ -16,26 +17,12 @@ import { SelectTag } from './select-objects'
const INPUT_STYLE = {
maxWidth: '8em',
}
const TAG_STYLE = {
backgroundColor: '#2598d9',
borderRadius: '0.5em',
color: 'white',
fontSize: '0.6em',
margin: '0.2em',
marginTop: '-0.1em',
padding: '0.3em',
verticalAlign: 'middle',
}
const LINK_STYLE = {
cursor: 'pointer',
}
const ADD_TAG_STYLE = {
cursor: 'pointer',
display: 'inline-block',
fontSize: '0.8em',
marginLeft: '0.2em',
}
const REMOVE_TAG_STYLE = {
cursor: 'pointer',
verticalAlign: 'middle',
}
class SelectExistingTag extends Component {
@@ -128,19 +115,26 @@ export default class Tags extends Component {
const deleteTag = (onDelete || onChange) && this._deleteTag
return (
<span className='form-group' style={{ color: '#999' }}>
<Icon icon='tags' />{' '}
<span>
<div style={{ color: '#999', display: 'inline-block' }}>
<div style={{ display: 'inline-block', verticalAlign: 'middle' }}>
<Icon icon='tags' />
</div>
<div style={{ display: 'inline-block', fontSize: '0.6em', verticalAlign: 'middle' }}>
{map(labels.sort(), (label, index) => (
<Tag label={label} onDelete={deleteTag} key={index} onClick={onClick} />
))}
</span>
</div>
{(onAdd || onChange) && !this.state.editing ? (
<span onClick={this._startEdit} style={ADD_TAG_STYLE}>
<div onClick={this._startEdit} style={ADD_TAG_STYLE}>
<Icon icon='add-tag' />
</span>
</div>
) : (
<span className='form-inline' onBlur={this._closeEditionIfUnfocused} onFocus={this._focus}>
<div
style={{ display: 'inline-block', verticalAlign: 'middle' }}
className='form-inline'
onBlur={this._closeEditionIfUnfocused}
onFocus={this._focus}
>
<span className='input-group'>
<input autoFocus className='form-control' onKeyDown={this._onKeyDown} style={INPUT_STYLE} type='text' />
<span className='input-group-btn'>
@@ -149,27 +143,97 @@ export default class Tags extends Component {
</Tooltip>
</span>
</span>
</span>
</div>
)}
</span>
</div>
)
}
}
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' />
</span>
) : (
[]
)}
</span>
)
export const Tag = ({
type,
label,
onDelete,
onClick,
// must be in format #rrggbb for luminance parsing
color = '#2598d9',
}) => {
const borderSize = '0.2em'
const padding = '0.2em'
const isLight =
relativeLuminance(
Array.from({ length: 3 }, (_, i) => {
const j = i * 2 + 1
return parseInt(color.slice(j, j + 2), 16)
})
) > 0.5
const i = label.indexOf('=')
const isScoped = i !== -1
return (
<div
style={{
background: color,
border: borderSize + ' solid ' + color,
borderRadius: '0.5em',
color: isLight ? '#000' : '#fff',
display: 'inline-block',
margin: '0.2em',
// prevent value background from breaking border radius
overflow: 'clip',
}}
>
<div
onClick={onClick && (() => onClick(label))}
style={{
cursor: onClick && 'pointer',
display: 'inline-block',
}}
>
<div
style={{
display: 'inline-block',
padding,
}}
>
{isScoped ? label.slice(0, i) : label}
</div>
{isScoped && (
<div
style={{
background: '#fff',
color: '#000',
display: 'inline-block',
padding,
}}
>
{label.slice(i + 1) || <i>N/A</i>}
</div>
)}
</div>
{onDelete && (
<div
onClick={onDelete && (() => onDelete(label))}
style={{
cursor: 'pointer',
display: 'inline-block',
padding,
// if isScoped, the display is a bit different
background: isScoped && '#fff',
color: isScoped && (isLight ? '#000' : color),
}}
>
<Icon icon='remove-tag' />
</div>
)}
</div>
)
}
Tag.propTypes = {
label: PropTypes.string.isRequired,
}

View File

@@ -335,9 +335,9 @@ export default class HostItem extends Component {
{host.productBrand} {host.version}
</Col>
<Col mediumSize={3} className={styles.itemExpanded}>
<span style={{ fontSize: '1.4em' }}>
<div style={{ fontSize: '1.4em' }}>
<HomeTags type='host' labels={host.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</div>
</Col>
<Col mediumSize={6} className={styles.itemExpanded}>
<MiniStats fetch={this._fetchStats} />

View File

@@ -265,9 +265,9 @@ export default class PoolItem extends Component {
{master.productBrand} {master.version}
</Col>
<Col mediumSize={5}>
<span style={{ fontSize: '1.4em' }}>
<div style={{ fontSize: '1.4em' }}>
<HomeTags type='pool' labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</div>
</Col>
<Col mediumSize={3} className={styles.itemExpanded}>
<span>

View File

@@ -192,9 +192,9 @@ export default class SrItem extends Component {
{sr.VDIs.length}x <Icon icon='disk' />
</Col>
<Col mediumSize={4}>
<span style={{ fontSize: '1.4em' }}>
<div style={{ fontSize: '1.4em' }}>
<HomeTags type='SR' labels={sr.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</div>
</Col>
</SingleLineRow>
)}

View File

@@ -87,9 +87,9 @@ export default class TemplateItem extends Component {
))}
</Col>
<Col mediumSize={4}>
<span style={{ fontSize: '1.4em' }}>
<div style={{ fontSize: '1.4em' }}>
<HomeTags type='VM-template' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</div>
</Col>
</Row>
)}

View File

@@ -209,9 +209,9 @@ export default class VmItem extends Component {
))}
</Col>
<Col mediumSize={6}>
<span style={{ fontSize: '1.4em' }}>
<div style={{ fontSize: '1.4em' }}>
<HomeTags type='VM' labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</div>
</Col>
<Col mediumSize={6} className={styles.itemExpanded}>
{this._isRunning && <MiniStats fetch={this._fetchStats} />}

View File

@@ -9654,6 +9654,11 @@ eslint@^8.7.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
esm@^3.0.84:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^9.0.0, espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -18394,6 +18399,13 @@ relateurl@0.2.x, relateurl@^0.2.7:
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==
relative-luminance@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/relative-luminance/-/relative-luminance-2.0.1.tgz#2babddf3a5a59673227d6f02e0f68e13989e3d13"
integrity sha512-wFuITNthJilFPwkK7gNJcULxXBcfFZvZORsvdvxeOdO44wCeZnuQkf3nFFzOR/dpJNxYsdRZJLsepWbyKhnMww==
dependencies:
esm "^3.0.84"
release-zalgo@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"