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
Delmer 2019-02-21 08:09:21 -05:00 committed by GitHub
parent 2e42203f07
commit 263170dc45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 364 additions and 94 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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