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:
@@ -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-->
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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} />}
|
||||
|
||||
12
yarn.lock
12
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user