diff --git a/CHANGELOG.md b/CHANGELOG.md index c9d8ff067d5..e38ef8dfea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ Once completed, `v2.0.0-alpha.5` can be started. 1. [12317](https://github.com/influxdata/influxdb/pull/12317): Redesign Create Scraper workflow 1. [12317](https://github.com/influxdata/influxdb/pull/12317): Show warning in Telegrafs and Scrapers lists when user has no buckets 1. [12384](https://github.com/influxdata/influxdb/pull/12384): Streamline label addition, removal, and creation from the dashboards list +1. [12464](https://github.com/influxdata/influxdb/pull/12464): Improve label color selection ## v2.0.0-alpha.4 [2019-02-21] diff --git a/ui/src/clockface/components/color_picker/ColorPicker.scss b/ui/src/clockface/components/color_picker/ColorPicker.scss new file mode 100644 index 00000000000..7f93baf66a5 --- /dev/null +++ b/ui/src/clockface/components/color_picker/ColorPicker.scss @@ -0,0 +1,100 @@ +@import 'src/style/modules'; + +/* + Color Picker Widget + ------------------------------------------------------------------------------ +*/ + +$color-picker--margin: 0; + +.color-picker { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + min-width: 160px; +} + +.color-picker--swatches { + margin-bottom: $ix-marg-b; + position: relative; + padding: $color-picker--margin; + border-radius: $radius; + overflow: hidden; + + &:hover { + cursor: pointer; + } +} + +.color-picker--swatch { + width: 10%; + padding-bottom: 10%; + position: relative; + float: left; + opacity: 1; + transition: opacity 0.25s ease; + + > span { + position: absolute; + top: $color-picker--margin; + left: $color-picker--margin; + right: $color-picker--margin; + bottom: $color-picker--margin; + } + + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0, 0); + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $g20-white; + opacity: 0; + transition: opacity 0.25s ease, transform 0.25s ease; + box-shadow: 0 0 4px 1px rgba($g0-obsidian, 0.25); + } + + &:hover { + &:after { + opacity: 1; + transform: translate(-50%, -50%) scale(1, 1); + } + } +} + +.color-picker--form { + display: flex; + align-items: center; + position: relative; +} + +.input.color-picker--input { + flex: 1 0 0; + margin-right: $ix-marg-a; + + > input { + padding-left: $ix-marg-d; + } +} + +.color-picker--selected { + pointer-events: none; + z-index: 2; + position: absolute; + top: 50%; + left: $ix-marg-c; + transform: translate(-50%, -50%); + width: 18px; + height: 18px; + border-radius: 50%; + border: $ix-border solid $g5-pepper; + transition: background-color 0.25s ease, border-color 0.25s ease; + + .input:hover + & { + border-color: $g7-graphite; + } +} diff --git a/ui/src/clockface/components/color_picker/ColorPicker.tsx b/ui/src/clockface/components/color_picker/ColorPicker.tsx new file mode 100644 index 00000000000..65005ac947b --- /dev/null +++ b/ui/src/clockface/components/color_picker/ColorPicker.tsx @@ -0,0 +1,184 @@ +// Libraries +import React, {Component, ChangeEvent} from 'react' +import _ from 'lodash' + +// Components +import { + Button, + IconFont, + ButtonShape, + ComponentStatus, +} from '@influxdata/clockface' +import {Input} from 'src/clockface' +import Swatch from 'src/clockface/components/color_picker/ColorPickerSwatch' +import Error from 'src/clockface/components/form_layout/FormElementError' + +// Constants +import {colors} from 'src/clockface/constants/colors' + +// Utils +import {validateHexCode} from 'src/configuration/utils/labels' + +// Styles +import 'src/clockface/components/color_picker/ColorPicker.scss' + +interface PassedProps { + color: string + onChange: (color: string, status?: ComponentStatus) => void +} + +interface DefaultProps { + maintainInputFocus?: boolean + testID?: string +} + +type Props = PassedProps & DefaultProps + +interface State { + errorMessage: string +} + +export default class ColorPicker extends Component { + public static defaultProps: DefaultProps = { + maintainInputFocus: false, + testID: 'color-picker', + } + + constructor(props: Props) { + super(props) + + this.state = { + errorMessage: null, + } + } + + render() { + const {maintainInputFocus, testID, color} = this.props + + return ( +
+
+ {colors.map(color => ( + + ))} +
+
+ + {this.colorPreview} +
+ {this.errorMessage} +
+ ) + } + + private get inputStatus(): ComponentStatus { + const {errorMessage} = this.state + + return errorMessage ? ComponentStatus.Error : ComponentStatus.Valid + } + + private handleSwatchClick = (hex: string): void => { + const {onChange} = this.props + + this.setState({errorMessage: null}) + onChange(hex, ComponentStatus.Valid) + } + + private handleInputChange = (e: ChangeEvent) => { + const {onChange} = this.props + const acceptedChars = [ + '#', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + ] + + const trimmedValue = e.target.value.trim() + const cleanedValue = trimmedValue + .split('') + .filter(char => acceptedChars.includes(char.toLowerCase())) + .join('') + + const errorMessage = validateHexCode(cleanedValue) + const status = errorMessage ? ComponentStatus.Error : ComponentStatus.Valid + + this.setState({errorMessage}) + onChange(cleanedValue, status) + } + + private handleInputBlur = (e: ChangeEvent) => { + const {maintainInputFocus} = this.props + + if (maintainInputFocus) { + e.target.focus() + } + } + + private handleRandomizeColor = (): void => { + const {onChange} = this.props + const {hex} = _.sample(colors) + + this.setState({errorMessage: null}) + onChange(hex, ComponentStatus.Valid) + } + + private get errorMessage(): JSX.Element { + const {testID} = this.props + const {errorMessage} = this.state + + if (errorMessage) { + return ( +
+ +
+ ) + } + } + + private get colorPreview(): JSX.Element { + const {color} = this.props + + return ( +
+ ) + } +} diff --git a/ui/src/clockface/components/color_picker/ColorPickerSwatch.tsx b/ui/src/clockface/components/color_picker/ColorPickerSwatch.tsx new file mode 100644 index 00000000000..4f0f779244b --- /dev/null +++ b/ui/src/clockface/components/color_picker/ColorPickerSwatch.tsx @@ -0,0 +1,29 @@ +// Libraries +import React, {Component} from 'react' + +interface Props { + name: string + hex: string + onClick: (hex: string) => void + testID: string +} + +export default class ColorPickerSwatch extends Component { + render() { + const {name, hex, testID} = this.props + return ( +
+ +
+ ) + } + + private handleClick = (): void => { + this.props.onClick(this.props.hex) + } +} diff --git a/ui/src/clockface/constants/colors.ts b/ui/src/clockface/constants/colors.ts new file mode 100644 index 00000000000..f4dcb721f2e --- /dev/null +++ b/ui/src/clockface/constants/colors.ts @@ -0,0 +1,207 @@ +export const colors = [ + // Row 1 + { + hex: '#311F94', + name: 'Void', + }, + { + hex: '#513CC6', + name: 'Amethyst', + }, + { + hex: '#7A65F2', + name: 'Star', + }, + { + hex: '#9394FF', + name: 'Comet', + }, + { + hex: '#B1B6FF', + name: 'Potassium', + }, + { + hex: '#C9D0FF', + name: 'Moonstone', + }, + { + hex: '#fafafc', + name: 'Ghost', + }, + { + hex: '#c6cad3', + name: 'Chromium', + }, + { + hex: '#757888', + name: 'Mountain', + }, + { + hex: '#31313d', + name: 'Onyx', + }, + // Row 2 + { + hex: '#326BBA', + name: 'Sapphire', + }, + { + hex: '#4591ED', + name: 'Ocean', + }, + { + hex: '#22ADF6', + name: 'Pool', + }, + { + hex: '#00C9FF', + name: 'Laser', + }, + { + hex: '#6BDFFF', + name: 'Hydrogen', + }, + { + hex: '#BEF0FF', + name: 'Neutrino', + }, + { + hex: '#f6f6f8', + name: 'Cloud', + }, + { + hex: '#bec2cc', + name: 'Mist', + }, + { + hex: '#676978', + name: 'Storm', + }, + { + hex: '#292933', + name: 'Castle', + }, + // Row 3 + { + hex: '#108174', + name: 'Emerald', + }, + { + hex: '#32B08C', + name: 'Viridian', + }, + { + hex: '#4ED8A0', + name: 'Rainforest', + }, + { + hex: '#7CE490', + name: 'Honeydew', + }, + { + hex: '#A5F3B4', + name: 'Krypton', + }, + { + hex: '#C6FFD0', + name: 'Wasabi', + }, + { + hex: '#eeeff2', + name: 'Whisper', + }, + { + hex: '#a4a8b6', + name: 'Forge', + }, + { + hex: '#545667', + name: 'Graphite', + }, + { + hex: '#202028', + name: 'Kevlar', + }, + // Row 4 + { + hex: '#E85B1C', + name: 'Topaz', + }, + { + hex: '#F48D38', + name: 'Tiger', + }, + { + hex: '#FFB94A', + name: 'Pineapple', + }, + { + hex: '#FFD255', + name: 'Thunder', + }, + { + hex: '#FFE480', + name: 'Sulfur', + }, + { + hex: '#FFF6B8', + name: 'Daisy', + }, + { + hex: '#e7e8eb', + name: 'Pearl', + }, + { + hex: '#999dab', + name: 'Sidewalk', + }, + { + hex: '#434453', + name: 'Smoke', + }, + { + hex: '#1c1c21', + name: 'Raven', + }, + // Row 5 + { + hex: '#BF3D5E', + name: 'Ruby', + }, + { + hex: '#DC4E58', + name: 'Fire', + }, + { + hex: '#F95F53', + name: 'Curacao', + }, + { + hex: '#FF8564', + name: 'Dreamsicle', + }, + { + hex: '#FFB6A0', + name: 'Tungsten', + }, + { + hex: '#FFDCCF', + name: 'Marmelade', + }, + { + hex: '#d4d7dd', + name: 'Platinum', + }, + { + hex: '#8e91a1', + name: 'Wolf', + }, + { + hex: '#383846', + name: 'Pepper', + }, + { + hex: '#0f0e15', + name: 'Obsidian', + }, +] diff --git a/ui/src/clockface/index.ts b/ui/src/clockface/index.ts index af2430e2cb8..8a680f824ff 100644 --- a/ui/src/clockface/index.ts +++ b/ui/src/clockface/index.ts @@ -2,6 +2,7 @@ import Alert from './components/alerts/Alert' import AutoInput from './components/auto_input/AutoInput' import ConfirmationButton from './components/confirmation_button/ConfirmationButton' +import ColorPicker from './components/color_picker/ColorPicker' import Dropdown, {DropdownMode} from './components/dropdowns/Dropdown' import MultiSelectDropdown from './components/dropdowns/MultiSelectDropdown' import Form from './components/form_layout/Form' @@ -61,6 +62,7 @@ export { AutoInput, ButtonType, ButtonShape, + ColorPicker, Columns, ComponentColor, ComponentSize, diff --git a/ui/src/configuration/components/CreateLabelOverlay.tsx b/ui/src/configuration/components/CreateLabelOverlay.tsx index fc93689d277..c943066b174 100644 --- a/ui/src/configuration/components/CreateLabelOverlay.tsx +++ b/ui/src/configuration/components/CreateLabelOverlay.tsx @@ -8,11 +8,9 @@ import { OverlayContainer, OverlayBody, OverlayHeading, + ComponentStatus, } from 'src/clockface' -// Utils -import {validateHexCode} from 'src/configuration/utils/labels' - // Types import {ILabel} from '@influxdata/influx' @@ -32,14 +30,18 @@ interface Props { interface State { label: ILabel - useCustomColorHex: boolean + colorStatus: ComponentStatus } @ErrorHandling class CreateLabelOverlay extends Component { - public state: State = { - label: {...EMPTY_LABEL, name: this.props.overrideDefaultName}, - useCustomColorHex: false, + constructor(props: Props) { + super(props) + + this.state = { + label: {...EMPTY_LABEL, name: this.props.overrideDefaultName}, + colorStatus: ComponentStatus.Default, + } } componentDidUpdate(prevProps) { @@ -56,21 +58,19 @@ class CreateLabelOverlay extends Component { public render() { const {isVisible, onDismiss, onNameValidation} = this.props - const {label, useCustomColorHex} = this.state + const {label} = this.state return ( - + { } private get isFormValid(): boolean { - const {label} = this.state + const {label, colorStatus} = this.state const nameIsValid = this.props.onNameValidation(label.name) === null - const colorIsValid = validateHexCode(label.properties.color) === null + const colorIsValid = + colorStatus === ComponentStatus.Default || + colorStatus === ComponentStatus.Valid return nameIsValid && colorIsValid } @@ -108,7 +110,6 @@ class CreateLabelOverlay extends Component { private resetForm() { this.setState({ label: EMPTY_LABEL, - useCustomColorHex: false, }) } @@ -132,15 +133,14 @@ class CreateLabelOverlay extends Component { } } - private handleColorHexChange = (color: string): void => { + private handleColorChange = ( + color: string, + colorStatus: ComponentStatus + ): void => { const properties = {...this.state.label.properties, color} const label = {...this.state.label, properties} - this.setState({label}) - } - - private handleToggleCustomColorHex = (useCustomColorHex: boolean): void => { - this.setState({useCustomColorHex}) + this.setState({label, colorStatus}) } } diff --git a/ui/src/configuration/components/LabelColorDropdown.scss b/ui/src/configuration/components/LabelColorDropdown.scss deleted file mode 100644 index 270cafadf99..00000000000 --- a/ui/src/configuration/components/LabelColorDropdown.scss +++ /dev/null @@ -1,23 +0,0 @@ -/* - Label Color Dropdown Styles - ------------------------------------------------------------------------------ -*/ - -@import 'src/style/modules'; - -.label-colors--item { - display: flex; - align-items: center; -} - -.label-colors--swatch, -.label-colors--custom { - width: 14px; - height: 14px; - border-radius: 50%; - margin-right: $ix-marg-a + $ix-border; -} - -.label-colors--custom { - border: $ix-border solid $g8-storm; -} diff --git a/ui/src/configuration/components/LabelColorDropdown.tsx b/ui/src/configuration/components/LabelColorDropdown.tsx deleted file mode 100644 index 43ff034bb87..00000000000 --- a/ui/src/configuration/components/LabelColorDropdown.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// Libraries -import React, {Component} from 'react' - -// Components -import {Dropdown, DropdownMenuColors} from 'src/clockface' - -// Constants -import { - PRESET_LABEL_COLORS, - CUSTOM_LABEL, -} from 'src/configuration/constants/LabelColors' - -// Types -import {LabelColor, LabelColorType} from 'src/types/colors' - -// Styles -import 'src/configuration/components/LabelColorDropdown.scss' - -// Decorators -import {ErrorHandling} from 'src/shared/decorators/errors' - -interface Props { - colorHex: string - onChange: (colorHex: string) => void - onToggleCustomColorHex: (useCustomColorHex: boolean) => void - useCustomColorHex: boolean -} - -@ErrorHandling -class LabelColorDropdown extends Component { - public render() { - return ( - - {PRESET_LABEL_COLORS.map(preset => { - if (preset.type === LabelColorType.Preset) { - return ( - -
-
- {preset.name} -
- - ) - } else if (preset.type === LabelColorType.Custom) { - return ( - -
-
- {preset.name} -
- - ) - } - })} - - ) - } - - private get selectedColorID(): string { - const {colorHex, useCustomColorHex} = this.props - - const foundPreset = PRESET_LABEL_COLORS.find( - preset => preset.colorHex === colorHex - ) - - if (useCustomColorHex || !foundPreset) { - return CUSTOM_LABEL.id - } - - return foundPreset.id - } - - private handleChange = (color: LabelColor): void => { - const {onChange, onToggleCustomColorHex} = this.props - const {colorHex, type} = color - - if (type === LabelColorType.Preset) { - onToggleCustomColorHex(false) - onChange(colorHex) - } else if (type === LabelColorType.Custom) { - onToggleCustomColorHex(true) - } - } -} - -export default LabelColorDropdown diff --git a/ui/src/configuration/components/LabelOverlayForm.scss b/ui/src/configuration/components/LabelOverlayForm.scss index dd31e0f6bb7..a24f2892c3e 100644 --- a/ui/src/configuration/components/LabelOverlayForm.scss +++ b/ui/src/configuration/components/LabelOverlayForm.scss @@ -8,10 +8,10 @@ .label-overlay--preview { display: flex; justify-content: center; - height: 50px; + align-items: center; + height: 90px; > .label { margin: $ix-marg-a 0; } } - diff --git a/ui/src/configuration/components/LabelOverlayForm.tsx b/ui/src/configuration/components/LabelOverlayForm.tsx index d23845a8c31..71546b3235a 100644 --- a/ui/src/configuration/components/LabelOverlayForm.tsx +++ b/ui/src/configuration/components/LabelOverlayForm.tsx @@ -3,31 +3,17 @@ import React, {PureComponent, ChangeEvent} from 'react' // Components import { - Stack, Button, Columns, - Alignment, ButtonType, ComponentSize, ComponentColor, ComponentStatus, } from '@influxdata/clockface' -import { - Grid, - Form, - Input, - ComponentSpacer, - Label, - InputType, -} from 'src/clockface' -import LabelColorDropdown from 'src/configuration/components/LabelColorDropdown' +import {Grid, Form, Input, Label, InputType, ColorPicker} from 'src/clockface' // Constants -import { - CUSTOM_LABEL, - HEX_CODE_CHAR_LENGTH, - INPUT_ERROR_COLOR, -} from 'src/configuration/constants/LabelColors' +import {INPUT_ERROR_COLOR} from 'src/configuration/constants/LabelColors' const MAX_LABEL_CHARS = 50 // Utils @@ -43,14 +29,12 @@ interface Props { id: string name: string description: string - colorHex: string + color: string + onColorChange: (color: string, status?: ComponentStatus) => void onSubmit: () => void onCloseModal: () => void onInputChange: (e: ChangeEvent) => void - onColorHexChange: (colorHex: string) => void - onToggleCustomColorHex: (useCustomColorHex: boolean) => void onNameValidation: (name: string) => string | null - useCustomColorHex: boolean buttonText: string isFormValid: boolean } @@ -61,14 +45,13 @@ export default class LabelOverlayForm extends PureComponent { const { id, name, + color, onSubmit, buttonText, description, onCloseModal, onInputChange, - onColorHexChange, - useCustomColorHex, - onToggleCustomColorHex, + onColorChange, isFormValid, } = this.props @@ -83,13 +66,13 @@ export default class LabelOverlayForm extends PureComponent { size={ComponentSize.Small} name={this.placeholderLabelName} description={description} - colorHex={this.colorHexGuard} + colorHex={this.colorGuard} id={id} /> - + { )} - - - - - {this.customColorInput} - - - { /> + + + + +