fix(ui): add label from resource labels editor (#11973)
Add ResourceLabelForm to EditLabelsOverlay and RandomLabelColorButton Co-authored-by: Daniel Campbell <metalwhirlwind@gmail.com>pull/12046/head
parent
2e42203f07
commit
263170dc45
|
@ -27,31 +27,14 @@
|
|||
|
||||
.label-selector--menu {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-height: 50px;
|
||||
margin: $ix-marg-b;
|
||||
}
|
||||
|
||||
.label-selector--menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $ix-marg-a $ix-marg-b;
|
||||
color: $g13-mist;
|
||||
transition: color 0.25s ease, background-color 0.25s ease;
|
||||
|
||||
&.active,
|
||||
&.active:hover {
|
||||
cursor: pointer;
|
||||
color: $g18-cloud;
|
||||
background-color: $g6-smoke;
|
||||
}
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.label-selector--label {
|
||||
flex: 4 0 0
|
||||
}
|
||||
|
||||
.label-selector--description,
|
||||
.label-selector--empty {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
@ -69,15 +52,10 @@
|
|||
line-height: 30px;
|
||||
}
|
||||
|
||||
.label-selector--description {
|
||||
margin-left: 6px;
|
||||
color: $g13-mist;
|
||||
flex: 2 0 50%;
|
||||
}
|
||||
|
||||
.label-selector--bottom {
|
||||
.label-selector--selection {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: $ix-marg-c;
|
||||
}
|
||||
|
||||
.label-selector--remove-all {
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
import React, {Component, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// APIs
|
||||
import {client} from 'src/utils/api'
|
||||
|
||||
// Components
|
||||
import {Button} from '@influxdata/clockface'
|
||||
import Input from 'src/clockface/components/inputs/Input'
|
||||
|
@ -12,7 +15,7 @@ import {ClickOutside} from 'src/shared/components/ClickOutside'
|
|||
|
||||
// Types
|
||||
import {ComponentSize} from 'src/clockface/types'
|
||||
import {Label as LabelAPI} from '@influxdata/influx'
|
||||
import {Label as APILabel} from 'src/types/v2/labels'
|
||||
|
||||
// Styles
|
||||
import './LabelSelector.scss'
|
||||
|
@ -25,18 +28,19 @@ enum ArrowDirection {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
selectedLabels: LabelAPI[]
|
||||
allLabels: LabelAPI[]
|
||||
onAddLabel: (label: LabelAPI) => void
|
||||
onRemoveLabel: (label: LabelAPI) => void
|
||||
selectedLabels: APILabel[]
|
||||
allLabels: APILabel[]
|
||||
onAddLabel: (label: APILabel) => void
|
||||
onRemoveLabel: (label: APILabel) => void
|
||||
onRemoveAllLabels: () => void
|
||||
onCreateLabel: (label: APILabel) => void
|
||||
resourceType: string
|
||||
inputSize?: ComponentSize
|
||||
}
|
||||
|
||||
interface State {
|
||||
filterValue: string
|
||||
filteredLabels: LabelAPI[]
|
||||
filteredLabels: APILabel[]
|
||||
isSuggesting: boolean
|
||||
highlightedID: string
|
||||
}
|
||||
|
@ -64,31 +68,21 @@ class LabelSelector extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {resourceType, inputSize} = this.props
|
||||
const {filterValue} = this.state
|
||||
public componentDidMount() {
|
||||
this.handleStartSuggesting()
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="label-selector">
|
||||
<ClickOutside onClickOutside={this.handleStopSuggesting}>
|
||||
<div className="label-selector--input">
|
||||
<Input
|
||||
placeholder={`Add labels to ${resourceType}`}
|
||||
value={filterValue}
|
||||
onFocus={this.handleStartSuggesting}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
size={inputSize}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{this.suggestionMenu}
|
||||
<ClickOutside onClickOutside={this.handleStopSuggesting}>
|
||||
<div className="label-selector">
|
||||
<div className="label-selector--selection">
|
||||
{this.selectedLabels}
|
||||
{this.clearSelectedButton}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
<div className="label-selector--bottom">
|
||||
{this.selectedLabels}
|
||||
{this.clearSelectedButton}
|
||||
{this.input}
|
||||
</div>
|
||||
</div>
|
||||
</ClickOutside>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -121,23 +115,45 @@ class LabelSelector extends Component<Props, State> {
|
|||
|
||||
private get suggestionMenu(): JSX.Element {
|
||||
const {allLabels, selectedLabels} = this.props
|
||||
const {isSuggesting, highlightedID} = this.state
|
||||
const {isSuggesting, highlightedID, filterValue} = this.state
|
||||
|
||||
const allLabelsUsed = allLabels.length === selectedLabels.length
|
||||
|
||||
if (isSuggesting) {
|
||||
return (
|
||||
<LabelSelectorMenu
|
||||
filterValue={filterValue}
|
||||
allLabelsUsed={allLabelsUsed}
|
||||
filteredLabels={this.availableLabels}
|
||||
highlightItemID={highlightedID}
|
||||
onItemClick={this.handleAddLabel}
|
||||
onItemHighlight={this.handleItemHighlight}
|
||||
onCreateLabel={this.handleCreateLabel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private get input(): JSX.Element {
|
||||
const {resourceType, inputSize} = this.props
|
||||
const {filterValue} = this.state
|
||||
|
||||
return (
|
||||
<div className="label-selector--input">
|
||||
<Input
|
||||
placeholder={`Add labels to ${resourceType}`}
|
||||
value={filterValue}
|
||||
onFocus={this.handleStartSuggesting}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onChange={this.handleInputChange}
|
||||
size={inputSize}
|
||||
autoFocus={true}
|
||||
/>
|
||||
{this.suggestionMenu}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleAddLabel = (labelID: string): void => {
|
||||
const {onAddLabel, allLabels} = this.props
|
||||
|
||||
|
@ -184,20 +200,7 @@ class LabelSelector extends Component<Props, State> {
|
|||
)
|
||||
|
||||
const filteredLabels = availableLabels.filter(label => {
|
||||
const filterChars = _.lowerCase(filterValue)
|
||||
.replace(/\s/g, '')
|
||||
.split('')
|
||||
const labelChars = _.lowerCase(label.name)
|
||||
.replace(/\s/g, '')
|
||||
.split('')
|
||||
|
||||
const overlap = _.difference(filterChars, labelChars)
|
||||
|
||||
if (overlap.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return label.name.includes(filterValue)
|
||||
})
|
||||
|
||||
const highlightedIDAvailable = filteredLabels.find(
|
||||
|
@ -208,10 +211,19 @@ class LabelSelector extends Component<Props, State> {
|
|||
highlightedID = filteredLabels[0].name
|
||||
}
|
||||
|
||||
if (filterValue.length === 0) {
|
||||
return this.setState({
|
||||
isSuggesting: true,
|
||||
filteredLabels: this.props.allLabels,
|
||||
highlightedID: null,
|
||||
filterValue: '',
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({filterValue, filteredLabels, highlightedID})
|
||||
}
|
||||
|
||||
private get availableLabels(): LabelAPI[] {
|
||||
private get availableLabels(): APILabel[] {
|
||||
const {selectedLabels} = this.props
|
||||
const {filteredLabels} = this.state
|
||||
|
||||
|
@ -288,6 +300,12 @@ class LabelSelector extends Component<Props, State> {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private handleCreateLabel = async (label: APILabel) => {
|
||||
const newLabel = await client.labels.create(label.name, label.properties)
|
||||
this.props.onAddLabel(newLabel)
|
||||
this.handleStopSuggesting()
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelSelector
|
||||
|
|
|
@ -5,19 +5,22 @@ import _ from 'lodash'
|
|||
// Components
|
||||
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
|
||||
import LabelSelectorMenuItem from 'src/clockface/components/label/LabelSelectorMenuItem'
|
||||
import ResourceLabelForm from 'src/shared/components/ResourceLabelForm'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Label} from '@influxdata/influx'
|
||||
import {Label} from 'src/types/v2/labels'
|
||||
|
||||
interface Props {
|
||||
filterValue: string
|
||||
highlightItemID: string
|
||||
filteredLabels: Label[]
|
||||
onItemClick: (labelID: string) => void
|
||||
onItemHighlight: (labelID: string) => void
|
||||
allLabelsUsed: boolean
|
||||
onCreateLabel: (label: Label) => Promise<void>
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -26,7 +29,10 @@ class LabelSelectorMenu extends Component<Props> {
|
|||
return (
|
||||
<div className="label-selector--menu-container">
|
||||
<FancyScrollbar autoHide={false} autoHeight={true} maxHeight={250}>
|
||||
<div className="label-selector--menu">{this.menuItems}</div>
|
||||
<div className="label-selector--menu">
|
||||
{this.resourceLabelForm}
|
||||
{this.menuItems}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
|
@ -67,6 +73,18 @@ class LabelSelectorMenu extends Component<Props> {
|
|||
|
||||
return 'No labels match your query'
|
||||
}
|
||||
|
||||
private get resourceLabelForm(): JSX.Element {
|
||||
const {filterValue, onCreateLabel, filteredLabels} = this.props
|
||||
|
||||
if (!filterValue || filteredLabels.find(l => l.name === filterValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<ResourceLabelForm labelName={filterValue} onSubmit={onCreateLabel} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LabelSelectorMenu
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Libraries
|
||||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
|
@ -24,30 +23,21 @@ class LabelSelectorMenuItem extends Component<Props> {
|
|||
const {name, colorHex, description, id} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={this.className}
|
||||
<span
|
||||
className="label-selector--menu-item"
|
||||
onMouseOver={this.handleMouseOver}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className="label-selector--label">
|
||||
<Label
|
||||
name={name}
|
||||
description={description}
|
||||
id={id}
|
||||
colorHex={colorHex}
|
||||
/>
|
||||
</div>
|
||||
<div className="label-selector--description">{description}</div>
|
||||
</div>
|
||||
<Label
|
||||
name={name}
|
||||
description={description}
|
||||
id={id}
|
||||
colorHex={colorHex}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {highlighted} = this.props
|
||||
|
||||
return classnames('label-selector--menu-item', {active: highlighted})
|
||||
}
|
||||
|
||||
private handleMouseOver = (): void => {
|
||||
const {onHighlight, id} = this.props
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
@import 'src/style/modules';
|
||||
|
||||
/*
|
||||
RandomLabelColor Styles
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.random-color--button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: $ix-marg-b;
|
||||
|
||||
.label-colors--swatch {
|
||||
margin-right: $ix-marg-c;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import React, {Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Utils
|
||||
import {randomPresetColor} from 'src/configuration/utils/labels'
|
||||
import {IconFont} from 'src/clockface'
|
||||
|
||||
// Styles
|
||||
import 'src/configuration/components/RandomLabelColor.scss'
|
||||
|
||||
interface Props {
|
||||
colorHex: string
|
||||
onClick: (newRandomHex: string) => void
|
||||
}
|
||||
|
||||
export default class RandomLabelColorButton extends Component<Props> {
|
||||
public render() {
|
||||
const {colorHex} = this.props
|
||||
return (
|
||||
<button
|
||||
className="button button-sm button-default random-color--button "
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div
|
||||
className="label-colors--swatch"
|
||||
style={{
|
||||
backgroundColor: colorHex,
|
||||
}}
|
||||
/>
|
||||
<span className={`button-icon icon ${IconFont.Refresh}`} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.props.onClick(randomPresetColor())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,13 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import {LabelType} from 'src/clockface'
|
||||
import {HEX_CODE_CHAR_LENGTH} from 'src/configuration/constants/LabelColors'
|
||||
import {
|
||||
HEX_CODE_CHAR_LENGTH,
|
||||
PRESET_LABEL_COLORS,
|
||||
} from 'src/configuration/constants/LabelColors'
|
||||
|
||||
export const randomPresetColor = () =>
|
||||
_.sample(PRESET_LABEL_COLORS.slice(1)).colorHex
|
||||
|
||||
export const validateLabelName = (
|
||||
labels: LabelType[],
|
||||
|
|
|
@ -23,7 +23,7 @@ import FetchLabels from 'src/shared/components/FetchLabels'
|
|||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Types
|
||||
import {Label} from '@influxdata/influx'
|
||||
import {Label} from 'src/types/v2/labels'
|
||||
import {RemoteDataState} from 'src/types'
|
||||
|
||||
// Utils
|
||||
|
|
|
@ -11,7 +11,7 @@ import {client} from 'src/utils/api'
|
|||
|
||||
// Types
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {Label} from '@influxdata/influx'
|
||||
import {Label} from 'src/types/v2/labels'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
@import 'src/style/modules';
|
||||
/*
|
||||
Resource Label Form styling
|
||||
---------------------------------------------
|
||||
*/
|
||||
|
||||
.resource-label--form {
|
||||
margin-bottom: $ix-marg-b;
|
||||
|
||||
.resource-label--create-button {
|
||||
margin-top: $ix-marg-c + 2px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
// Libraries
|
||||
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
// Components
|
||||
import {
|
||||
Button,
|
||||
ComponentColor,
|
||||
ButtonType,
|
||||
Columns,
|
||||
ComponentStatus,
|
||||
} from '@influxdata/clockface'
|
||||
import {
|
||||
Grid,
|
||||
Form,
|
||||
Input,
|
||||
InputType,
|
||||
ComponentSpacer,
|
||||
Alignment,
|
||||
} from 'src/clockface'
|
||||
import RandomLabelColorButton from 'src/configuration/components/RandomLabelColor'
|
||||
import {Label, LabelProperties} from 'src/types/v2/labels'
|
||||
|
||||
// Constants
|
||||
import {HEX_CODE_CHAR_LENGTH} from 'src/configuration/constants/LabelColors'
|
||||
|
||||
// Utils
|
||||
import {
|
||||
validateHexCode,
|
||||
randomPresetColor,
|
||||
} from 'src/configuration/utils/labels'
|
||||
|
||||
// Decorators
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
// Style
|
||||
import 'src/shared/components/ResourceLabelForm.scss'
|
||||
|
||||
interface Props {
|
||||
labelName: string
|
||||
onSubmit: (label: Label) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
isValid: boolean
|
||||
label: Label
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class ResourceLabelForm extends PureComponent<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isValid: false,
|
||||
label: {
|
||||
name: props.labelName,
|
||||
properties: {
|
||||
description: '',
|
||||
color: randomPresetColor(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (this.props.labelName !== this.state.label.name) {
|
||||
this.setState({label: {...this.state.label, name: this.props.labelName}})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isValid} = this.state
|
||||
|
||||
return (
|
||||
<div className="resource-label--form">
|
||||
<Grid.Row>
|
||||
<Grid.Column widthXS={Columns.Five}>
|
||||
<Form.Element label="Color">
|
||||
<ComponentSpacer stretchToFitWidth={true} align={Alignment.Left}>
|
||||
<RandomLabelColorButton
|
||||
colorHex={this.colorHex}
|
||||
onClick={this.handleColorChange}
|
||||
/>
|
||||
{this.customColorInput}
|
||||
</ComponentSpacer>
|
||||
</Form.Element>
|
||||
</Grid.Column>
|
||||
<Grid.Column widthXS={Columns.Five}>
|
||||
<Form.Element label="Description">
|
||||
<Input
|
||||
type={InputType.Text}
|
||||
placeholder="Add a optional description"
|
||||
name="description"
|
||||
value={this.description}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</Form.Element>
|
||||
</Grid.Column>
|
||||
<Grid.Column widthXS={Columns.Two}>
|
||||
<Form.Element label="">
|
||||
<Button
|
||||
customClass="resource-label--create-button"
|
||||
text="Create Label"
|
||||
color={ComponentColor.Success}
|
||||
type={ButtonType.Submit}
|
||||
status={
|
||||
isValid ? ComponentStatus.Default : ComponentStatus.Disabled
|
||||
}
|
||||
onClick={this.handleSubmit}
|
||||
/>
|
||||
</Form.Element>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.props.onSubmit(this.state.label)
|
||||
}
|
||||
|
||||
private handleCustomColorChange = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
const {value} = e.target
|
||||
|
||||
if (validateHexCode(value)) {
|
||||
this.setState({isValid: false})
|
||||
} else {
|
||||
this.setState({isValid: true})
|
||||
}
|
||||
|
||||
this.updateProperties({color: value})
|
||||
}
|
||||
|
||||
private handleColorChange = (color: string) => {
|
||||
this.setState({isValid: true})
|
||||
|
||||
this.updateProperties({color})
|
||||
}
|
||||
|
||||
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
this.updateProperties({description: e.target.value})
|
||||
}
|
||||
|
||||
private updateProperties(update: Partial<LabelProperties>) {
|
||||
const {label} = this.state
|
||||
|
||||
this.setState({
|
||||
label: {
|
||||
...label,
|
||||
properties: {...label.properties, ...update},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private get customColorInput(): JSX.Element {
|
||||
const {colorHex} = this
|
||||
|
||||
return (
|
||||
<Form.ValidationElement
|
||||
label=""
|
||||
value={colorHex}
|
||||
validationFunc={validateHexCode}
|
||||
>
|
||||
{status => (
|
||||
<Input
|
||||
type={InputType.Text}
|
||||
value={colorHex}
|
||||
placeholder="#000000"
|
||||
onChange={this.handleCustomColorChange}
|
||||
status={status}
|
||||
maxLength={HEX_CODE_CHAR_LENGTH}
|
||||
/>
|
||||
)}
|
||||
</Form.ValidationElement>
|
||||
)
|
||||
}
|
||||
|
||||
private get colorHex(): string {
|
||||
return this.state.label.properties.color
|
||||
}
|
||||
|
||||
private get description(): string {
|
||||
return this.state.label.properties.description
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue