Commit 2aae2556 by Alexander Zobnin Committed by Torkel Ödegaard

Unified colorpicker (#9347)

* colorpicker: initial picker with predefined palette and spectrum

* colorpicker: highlight selected color

* colorpicker: add onChange() callback

* colorpicker: replace singlestat picker by new one

* colorpicker: style tweak

* colorpicker: parse color on input blur

* colorpicker: sort palette by hue and lightness

* colorpicker: refactor, move colors sorting to 'app/core/utils/colors'

* tech: colorpicker - fix linter errors

* colorpicker: convert to React components

* colorpicker: fix spectrum import after moving to webpack

* colorpicker: minor refactor

* colorpicker: initial series color picker

* colorpicker: fix tests error
parent 7859d4ff
import React from 'react';
import coreModule from 'app/core/core_module';
import { sortedColors } from 'app/core/utils/colors';
export interface IProps {
color: string;
onColorSelect: (c: string) => void;
}
export class GfColorPalette extends React.Component<IProps, any> {
paletteColors: string[];
constructor(props) {
super(props);
this.paletteColors = sortedColors;
this.onColorSelect = this.onColorSelect.bind(this);
}
onColorSelect(color) {
return () => {
this.props.onColorSelect(color);
};
}
render() {
const colorPaletteItems = this.paletteColors.map((paletteColor) => {
const cssClass = paletteColor.toLowerCase() === this.props.color.toLowerCase() ? 'fa-circle-o' : 'fa-circle';
return (
<i key={paletteColor} className={"pointer fa " + cssClass}
style={{'color': paletteColor}}
onClick={this.onColorSelect(paletteColor)}>&nbsp;
</i>
);
});
return (
<div className="graph-legend-popover">
<p className="m-b-0">{colorPaletteItems}</p>
</div>
);
}
}
coreModule.directive('gfColorPalette', function (reactDirective) {
return reactDirective(GfColorPalette, ['color', 'onColorSelect']);
});
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import Drop from 'tether-drop';
import coreModule from 'app/core/core_module';
import { ColorPickerPopover } from './ColorPickerPopover';
export interface IProps {
color: string;
onChange: (c: string) => void;
}
export class ColorPicker extends React.Component<IProps, any> {
pickerElem: any;
colorPickerDrop: any;
constructor(props) {
super(props);
this.openColorPicker = this.openColorPicker.bind(this);
this.closeColorPicker = this.closeColorPicker.bind(this);
this.setPickerElem = this.setPickerElem.bind(this);
this.onColorSelect = this.onColorSelect.bind(this);
}
setPickerElem(elem) {
this.pickerElem = $(elem);
}
openColorPicker() {
const dropContent = (
<ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />
);
let dropContentElem = document.createElement('div');
ReactDOM.render(dropContent, dropContentElem);
let drop = new Drop({
target: this.pickerElem[0],
content: dropContentElem,
position: 'top center',
classes: 'drop-popover drop-popover--form',
openOn: 'hover',
hoverCloseDelay: 200,
tetherOptions: {
constraints: [{ to: 'scrollParent', attachment: "none both" }]
}
});
drop.on('close', this.closeColorPicker);
this.colorPickerDrop = drop;
this.colorPickerDrop.open();
}
closeColorPicker() {
setTimeout(() => {
if (this.colorPickerDrop && this.colorPickerDrop.tether) {
this.colorPickerDrop.destroy();
}
}, 100);
}
onColorSelect(color) {
this.props.onChange(color);
}
render() {
return (
<div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
<div className="sp-preview">
<div className="sp-preview-inner" style={{backgroundColor: this.props.color}}>
</div>
</div>
</div>
);
}
}
coreModule.directive('colorPicker', function (reactDirective) {
return reactDirective(ColorPicker, ['color', 'onChange']);
});
import React from 'react';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
import { GfColorPalette } from './ColorPalette';
import { GfSpectrumPicker } from './SpectrumPicker';
// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also
declare var tinycolor;
export interface IProps {
color: string;
onColorSelect: (c: string) => void;
}
export class ColorPickerPopover extends React.Component<IProps, any> {
pickerNavElem: any;
constructor(props) {
super(props);
this.state = {
tab: 'palette',
color: this.props.color,
colorString: this.props.color
};
this.onColorStringChange = this.onColorStringChange.bind(this);
this.onColorStringBlur = this.onColorStringBlur.bind(this);
this.sampleColorSelected = this.sampleColorSelected.bind(this);
this.spectrumColorSelected = this.spectrumColorSelected.bind(this);
this.setPickerNavElem = this.setPickerNavElem.bind(this);
}
setPickerNavElem(elem) {
this.pickerNavElem = $(elem);
}
setColor(color) {
let newColor = tinycolor(color);
if (newColor.isValid()) {
this.setState({
color: newColor.toString(),
colorString: newColor.toString()
});
this.props.onColorSelect(color);
}
}
sampleColorSelected(color) {
this.setColor(color);
}
spectrumColorSelected(color) {
let rgbColor = color.toRgbString();
this.setColor(rgbColor);
}
onColorStringChange(e) {
let colorString = e.target.value;
this.setState({
colorString: colorString
});
let newColor = tinycolor(colorString);
if (newColor.isValid()) {
// Update only color state
this.setState({
color: newColor.toString(),
});
this.props.onColorSelect(newColor);
}
}
onColorStringBlur(e) {
let colorString = e.target.value;
this.setColor(colorString);
}
componentDidMount() {
this.pickerNavElem.find('li:first').addClass('active');
this.pickerNavElem.on('show', (e) => {
// use href attr (#name => name)
let tab = e.target.hash.slice(1);
this.setState({
tab: tab
});
});
}
render() {
const paletteTab = (
<div id="palette">
<GfColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected} />
</div>
);
const spectrumTab = (
<div id="spectrum">
<GfSpectrumPicker color={this.props.color} onColorSelect={this.spectrumColorSelected} options={{}} />
</div>
);
const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
return (
<div className="gf-color-picker">
<ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem}>
<li className="gf-tabs-item-colorpicker">
<a href="#palette" data-toggle="tab">Colors</a>
</li>
<li className="gf-tabs-item-colorpicker">
<a href="#spectrum" data-toggle="tab">Spectrum</a>
</li>
</ul>
<div className="colorpicker-container">
{currentTab}
</div>
<div className="color-model-container">
<input className="gf-form-input" value={this.state.colorString}
onChange={this.onColorStringChange} onBlur={this.onColorStringBlur}>
</input>
</div>
</div>
);
}
}
coreModule.directive('gfColorPickerPopover', function (reactDirective) {
return reactDirective(ColorPickerPopover, ['color', 'onColorSelect']);
});
import React from 'react';
import coreModule from 'app/core/core_module';
import { ColorPickerPopover } from './ColorPickerPopover';
export interface IProps {
series: any;
onColorChange: (color: string) => void;
onToggleAxis: () => void;
}
export class SeriesColorPicker extends React.Component<IProps, any> {
constructor(props) {
super(props);
this.onColorChange = this.onColorChange.bind(this);
this.onToggleAxis = this.onToggleAxis.bind(this);
}
onColorChange(color) {
this.props.onColorChange(color);
}
onToggleAxis() {
this.props.onToggleAxis();
}
render() {
const leftButtonClass = this.props.series.yaxis === 1 ? 'btn-success' : 'btn-inverse';
const rightButtonClass = this.props.series.yaxis === 2 ? 'btn-success' : 'btn-inverse';
return (
<div className="graph-legend-popover">
<div className="p-b-1">
<label>Y Axis:</label>
<button onClick={this.onToggleAxis} className={"btn btn-small " + leftButtonClass}>
Left
</button>
<button onClick={this.onToggleAxis} className={"btn btn-small " + rightButtonClass}>
Right
</button>
</div>
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
</div>
);
}
}
coreModule.directive('seriesColorPicker', function (reactDirective) {
return reactDirective(SeriesColorPicker, ['series', 'onColorChange', 'onToggleAxis']);
});
import React from 'react';
import coreModule from 'app/core/core_module';
import _ from 'lodash';
import $ from 'jquery';
import 'vendor/spectrum';
export interface IProps {
color: string;
options: object;
onColorSelect: (c: string) => void;
}
export class GfSpectrumPicker extends React.Component<IProps, any> {
elem: any;
isMoving: boolean;
constructor(props) {
super(props);
this.onSpectrumMove = this.onSpectrumMove.bind(this);
this.setComponentElem = this.setComponentElem.bind(this);
}
setComponentElem(elem) {
this.elem = $(elem);
}
onSpectrumMove(color) {
this.isMoving = true;
this.props.onColorSelect(color);
}
componentDidMount() {
let spectrumOptions = _.assignIn({
flat: true,
showAlpha: true,
showButtons: false,
color: this.props.color,
appendTo: this.elem,
move: this.onSpectrumMove,
}, this.props.options);
this.elem.spectrum(spectrumOptions);
this.elem.spectrum('show');
this.elem.spectrum('set', this.props.color);
}
componentWillUpdate(nextProps) {
// If user move pointer over spectrum field this produce 'move' event and component
// may update props.color. We don't want to update spectrum color in this case, so we can use
// isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
// is called after updating occurs (when user finished moving).
if (!this.isMoving) {
this.elem.spectrum('set', nextProps.color);
}
}
componentDidUpdate() {
if (this.isMoving) {
this.isMoving = false;
}
}
componentWillUnmount() {
this.elem.spectrum('destroy');
}
render() {
return (
<div className="spectrum-container" ref={this.setComponentElem}></div>
);
}
}
coreModule.directive('gfSpectrumPicker', function (reactDirective) {
return reactDirective(GfSpectrumPicker, ['color', 'options', 'onColorSelect']);
});
...@@ -16,6 +16,8 @@ import './partials'; ...@@ -16,6 +16,8 @@ import './partials';
import './components/jsontree/jsontree'; import './components/jsontree/jsontree';
import './components/code_editor/code_editor'; import './components/code_editor/code_editor';
import './utils/outline'; import './utils/outline';
import './components/colorpicker/ColorPicker';
import './components/colorpicker/SeriesColorPicker';
import {grafanaAppDirective} from './components/grafana_app'; import {grafanaAppDirective} from './components/grafana_app';
import {sideMenuDirective} from './components/sidemenu/sidemenu'; import {sideMenuDirective} from './components/sidemenu/sidemenu';
......
import _ from 'lodash';
// Spectrum picker uses TinyColor and loads it as a global variable, so we can use it here also
declare var tinycolor;
export default [ export const PALETTE_ROWS = 4;
export const PALETTE_COLUMNS = 14;
let colors = [
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0",
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477",
"#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0",
...@@ -10,3 +16,26 @@ export default [ ...@@ -10,3 +16,26 @@ export default [
"#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7"
]; ];
export function sortColorsByHue(hexColors) {
let hslColors = _.map(hexColors, hexToHsl);
let sortedHSLColors = _.sortBy(hslColors, ['h']);
sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS);
sortedHSLColors = _.map(sortedHSLColors, chunk => {
return _.sortBy(chunk, 'l');
});
sortedHSLColors = _.flattenDeep(_.zip(...sortedHSLColors));
return _.map(sortedHSLColors, hslToHex);
}
export function hexToHsl(color) {
return tinycolor(color).toHsl();
}
export function hslToHex(color) {
return tinycolor(color).toHexString();
}
export let sortedColors = sortColorsByHue(colors);
export default colors;
...@@ -45,7 +45,8 @@ function (angular, _, $) { ...@@ -45,7 +45,8 @@ function (angular, _, $) {
popoverSrv.show({ popoverSrv.show({
element: el[0], element: el[0],
position: 'bottom center', position: 'bottom center',
template: '<gf-color-picker></gf-color-picker>', template: '<series-color-picker series="series" onToggleAxis="toggleAxis" onColorChange="colorSelected">' +
'</series-color-picker>',
openOn: 'hover', openOn: 'hover',
model: { model: {
series: series, series: series,
......
...@@ -69,13 +69,13 @@ ...@@ -69,13 +69,13 @@
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-8">Colors</label> <label class="gf-form-label width-8">Colors</label>
<span class="gf-form-label"> <span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[0]" ng-change="ctrl.render()" ></spectrum-picker> <color-picker color="ctrl.panel.colors[0]" onChange="ctrl.onColorChange(0)"></color-picker>
</span> </span>
<span class="gf-form-label"> <span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[1]" ng-change="ctrl.render()" ></spectrum-picker> <color-picker color="ctrl.panel.colors[1]" onChange="ctrl.onColorChange(1)"></color-picker>
</span> </span>
<span class="gf-form-label"> <span class="gf-form-label">
<spectrum-picker ng-model="ctrl.panel.colors[2]" ng-change="ctrl.render()" ></spectrum-picker> <color-picker color="ctrl.panel.colors[2]" onChange="ctrl.onColorChange(2)"></color-picker>
</span> </span>
<span class="gf-form-label"> <span class="gf-form-label">
<a ng-click="ctrl.invertColorOrder()"> <a ng-click="ctrl.invertColorOrder()">
......
...@@ -214,6 +214,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { ...@@ -214,6 +214,13 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.render(); this.render();
} }
onColorChange(panelColorIndex) {
return (color) => {
this.panel.colors[panelColorIndex] = color;
this.render();
};
}
getDecimalsForValue(value) { getDecimalsForValue(value) {
if (_.isNumber(this.panel.decimals)) { if (_.isNumber(this.panel.decimals)) {
return {decimals: this.panel.decimals, scaledDecimals: null}; return {decimals: this.panel.decimals, scaledDecimals: null};
......
...@@ -35,3 +35,13 @@ ...@@ -35,3 +35,13 @@
float: left; float: left;
z-index: 0; z-index: 0;
} }
.colorpicker-container {
min-height: 190px;
}
.drop-popover.gf-color-picker {
.drop-content {
width: 210px;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment